Sfoglia il codice sorgente

Major rewrite of stock alerts

* They are now known as 'stock alerts' instead of notifications.
* They have been moved into the customer app largely (instead of their
  own one).
* The model implementation has been simplified.
master
David Winterbottom 13 anni fa
parent
commit
9d60bef005
71 ha cambiato i file con 14077 aggiunte e 12554 eliminazioni
  1. 0
    1
      oscar/__init__.py
  2. 1
    15
      oscar/apps/catalogue/app.py
  3. 0
    157
      oscar/apps/catalogue/notification/abstract_models.py
  4. 0
    38
      oscar/apps/catalogue/notification/app.py
  5. 0
    25
      oscar/apps/catalogue/notification/forms.py
  6. 0
    0
      oscar/apps/catalogue/notification/migrations/__init__.py
  7. 0
    8
      oscar/apps/catalogue/notification/models.py
  8. 0
    23
      oscar/apps/catalogue/notification/receivers.py
  9. 0
    96
      oscar/apps/catalogue/notification/utils.py
  10. 0
    295
      oscar/apps/catalogue/notification/views.py
  11. 7
    9
      oscar/apps/catalogue/views.py
  12. 118
    0
      oscar/apps/customer/abstract_models.py
  13. 0
    0
      oscar/apps/customer/alerts/__init__.py
  14. 28
    0
      oscar/apps/customer/alerts/receivers.py
  15. 103
    0
      oscar/apps/customer/alerts/utils.py
  16. 90
    0
      oscar/apps/customer/alerts/views.py
  17. 15
    0
      oscar/apps/customer/app.py
  18. 58
    0
      oscar/apps/customer/forms.py
  19. 67
    31
      oscar/apps/customer/migrations/0003_auto__add_productalert.py
  20. 10
    6
      oscar/apps/customer/models.py
  21. 16
    39
      oscar/apps/customer/views.py
  22. 17
    13
      oscar/apps/dashboard/users/app.py
  23. 18
    6
      oscar/apps/dashboard/users/forms.py
  24. 27
    32
      oscar/apps/dashboard/users/views.py
  25. 4
    6
      oscar/defaults.py
  26. 1859
    1518
      oscar/locale/da/LC_MESSAGES/django.po
  27. 1859
    1518
      oscar/locale/de/LC_MESSAGES/django.po
  28. 1859
    1518
      oscar/locale/es/LC_MESSAGES/django.po
  29. 1859
    1518
      oscar/locale/fr/LC_MESSAGES/django.po
  30. 1859
    1518
      oscar/locale/it/LC_MESSAGES/django.po
  31. 1863
    1517
      oscar/locale/pl/LC_MESSAGES/django.po
  32. 1863
    1517
      oscar/locale/ru/LC_MESSAGES/django.po
  33. 16
    18
      oscar/management/commands/oscar_cleanup_alerts.py
  34. 25
    0
      oscar/management/commands/oscar_send_alerts.py
  35. 0
    40
      oscar/management/commands/oscar_send_notifications.py
  36. 2
    2
      oscar/templates/oscar/catalogue/browse.html
  37. 3
    4
      oscar/templates/oscar/catalogue/detail.html
  38. 7
    0
      oscar/templates/oscar/catalogue/partials/add_to_basket_form.html
  39. 1
    1
      oscar/templates/oscar/catalogue/partials/product.html
  40. 2
    7
      oscar/templates/oscar/catalogue/partials/stock_record.html
  41. 13
    0
      oscar/templates/oscar/customer/alerts/emails/alert_body.txt
  42. 1
    0
      oscar/templates/oscar/customer/alerts/emails/alert_subject.txt
  43. 13
    0
      oscar/templates/oscar/customer/alerts/emails/confirmation_body.txt
  44. 1
    0
      oscar/templates/oscar/customer/alerts/emails/confirmation_subject.txt
  45. 1
    0
      oscar/templates/oscar/customer/alerts/form.html
  46. 4
    0
      oscar/templates/oscar/customer/alerts/message.html
  47. 40
    4
      oscar/templates/oscar/customer/profile.html
  48. 0
    61
      oscar/templates/oscar/dashboard/notification/delete.html
  49. 0
    98
      oscar/templates/oscar/dashboard/notification/list.html
  50. 0
    40
      oscar/templates/oscar/dashboard/notification/update.html
  51. 40
    0
      oscar/templates/oscar/dashboard/users/alerts/delete.html
  52. 89
    0
      oscar/templates/oscar/dashboard/users/alerts/list.html
  53. 44
    0
      oscar/templates/oscar/dashboard/users/alerts/partials/alert.html
  54. 43
    0
      oscar/templates/oscar/dashboard/users/alerts/update.html
  55. 0
    32
      oscar/templates/oscar/notification/email.html
  56. 0
    59
      oscar/templates/oscar/notification/notification.html
  57. 0
    30
      oscar/templates/oscar/notification/notification_email.html
  58. 0
    12
      oscar/templates/oscar/notification/partials/notification_form.html
  59. 0
    54
      oscar/templates/oscar/notification/partials/notification_pane.html
  60. 0
    6
      oscar/templates/oscar/notification/partials/notified_date.html
  61. 0
    7
      oscar/templates/oscar/notification/partials/status.html
  62. 0
    7
      oscar/templates/oscar/notification/partials/user_link.html
  63. 0
    21
      oscar/templatetags/notification_tags.py
  64. 0
    19
      sites/sandbox/recreate_db.sh
  65. 37
    10
      tests/functional/catalogue/catalogue_tests.py
  66. 0
    122
      tests/functional/catalogue/notification_command_tests.py
  67. 0
    308
      tests/functional/catalogue/notification_tests.py
  68. 77
    0
      tests/functional/customer/alert_tests.py
  69. 0
    49
      tests/functional/customer/notification_tests.py
  70. 18
    0
      tests/unit/customer/alert_tests.py
  71. 0
    119
      tests/unit/notification_tests.py

+ 0
- 1
oscar/__init__.py Vedi File

@@ -37,7 +37,6 @@ OSCAR_CORE_APPS = [
37 37
     'oscar.apps.shipping',
38 38
     'oscar.apps.catalogue',
39 39
     'oscar.apps.catalogue.reviews',
40
-    'oscar.apps.catalogue.notification',
41 40
     'oscar.apps.basket',
42 41
     'oscar.apps.payment',
43 42
     'oscar.apps.offer',

+ 1
- 15
oscar/apps/catalogue/app.py Vedi File

@@ -3,7 +3,6 @@ from django.conf.urls import patterns, url, include
3 3
 from oscar.core.application import Application
4 4
 from oscar.apps.catalogue.views import ProductDetailView, ProductListView, ProductCategoryView
5 5
 from oscar.apps.catalogue.reviews.app import application as reviews_app
6
-from oscar.apps.catalogue.notification.app import application as notification_app
7 6
 
8 7
 
9 8
 class BaseCatalogueApplication(Application):
@@ -37,20 +36,7 @@ class ReviewsApplication(Application):
37 36
         return self.post_process_urls(urlpatterns)
38 37
 
39 38
 
40
-class NotificationsApplication(Application):
41
-    name = None
42
-    notification_app = notification_app
43
-
44
-    def get_urls(self):
45
-        urlpatterns = super(NotificationsApplication, self).get_urls()
46
-        urlpatterns += patterns('',
47
-            url(r'^(?P<product_slug>[\w-]*)_(?P<pk>\d+)/notify-me/',
48
-                include(self.notification_app.urls)),
49
-        )
50
-        return self.post_process_urls(urlpatterns)
51
-
52
-
53
-class CatalogueApplication(BaseCatalogueApplication, ReviewsApplication, NotificationsApplication):
39
+class CatalogueApplication(BaseCatalogueApplication, ReviewsApplication):
54 40
     """
55 41
     Composite class combining Products with Reviews
56 42
     """

+ 0
- 157
oscar/apps/catalogue/notification/abstract_models.py Vedi File

@@ -1,157 +0,0 @@
1
-import sha
2
-import random
3
-
4
-from django.db import models
5
-from django.contrib.auth.models import User
6
-from django.utils.translation import ugettext as _
7
-
8
-Product = models.get_model('catalogue', 'Product')
9
-
10
-
11
-class AbstractProductNotification(models.Model):
12
-    """
13
-    Abstract class of a product notification
14
-    A notification can have two different statuses for authenticated
15
-    users (``ACTIVE`` and ``INACTIVE`` and anonymous users have an
16
-    additional status ``UNCONFIRMED``. For anonymous users a confirmation
17
-    and unsubscription key are generated when an instance is saved for
18
-    the first time and can be used to confirm and unsubscribe the
19
-    notifications.
20
-    """
21
-    KEY_LENGTH = 40
22
-    product_url_index = 'catalogue:detail'
23
-
24
-    # a user is only required if the notification is created by a
25
-    # registered user, anonymous users will only have an email address
26
-    # attached to the notification
27
-    user = models.ForeignKey(User, db_index=True, blank=True, null=True,
28
-                             related_name="product_notifications")
29
-    product = models.ForeignKey(Product, db_index=True)
30
-
31
-    # These fields only apply to unauthenticated users and are empty
32
-    # if the user is registered.
33
-    email = models.EmailField(db_index=True, blank=True, null=True)
34
-    confirm_key = models.CharField(max_length=KEY_LENGTH, null=True)
35
-    unsubscribe_key = models.CharField(max_length=KEY_LENGTH, null=True)
36
-
37
-    UNCONFIRMED, ACTIVE, INACTIVE = ('unconfirmed', 'active', 'inactive')
38
-    STATUS_TYPES = (
39
-        (UNCONFIRMED, _('Not yet confirmed')),
40
-        (ACTIVE, _('Active')),
41
-        (INACTIVE, _('Inactive')),
42
-    )
43
-    status = models.CharField(max_length=20, choices=STATUS_TYPES,
44
-                              default=INACTIVE)
45
-
46
-    date_created = models.DateTimeField(auto_now_add=True)
47
-    date_modified = models.DateTimeField(auto_now=True)
48
-    date_notified = models.DateTimeField(blank=True, null=True)
49
-
50
-    def is_active(self):
51
-        """
52
-        Check if the notification is active or not.
53
-        Returns ``True`` if notification is active, ``False`` otherwise.
54
-        """
55
-        return self.status == self.ACTIVE
56
-
57
-    def is_confirmed(self):
58
-        """
59
-        Check if the notification is confirmed or not.
60
-        Returns ``True`` if notification is confirmed, ``False`` otherwise.
61
-        """
62
-        return self.status == self.ACTIVE
63
-
64
-    def get_random_key(self):
65
-        """
66
-        Get a random generated key based on SHA-1 and the notification
67
-        email provided in this instance of the notification.
68
-        """
69
-        salt = sha.new(str(random.random())).hexdigest()
70
-        return sha.new(salt + self.get_notification_email()).hexdigest()
71
-
72
-    def get_notification_email(self):
73
-        """
74
-        Return the notification email address of the user subscribed to
75
-        this notification. Return the user's email for an authenticated
76
-        user and the email property for an anonymous user.
77
-        """
78
-        if self.user:
79
-            return self.user.email
80
-        return self.email
81
-
82
-    def transfer_to_user(self, user):
83
-        """
84
-        Convenience function that allows for assigning a notification
85
-        to a user. This is aimed at the situation when a user has
86
-        notifications available as anonymous user but decides to sign
87
-        up. In this case, the notification will be transfered to the
88
-        specific user account.
89
-        """
90
-        if not self.user:
91
-            self.user = user
92
-            self.email = None
93
-            self.confirm_key, self.unsubscribe_key = None, None
94
-            self.save()
95
-
96
-    @models.permalink
97
-    def get_confirm_url(self):
98
-        """
99
-        Get confirmation URL for this notification.
100
-        """
101
-        kwargs = self._get_url_kwargs()
102
-        kwargs['key'] = self.confirm_key
103
-        return ('catalogue:notification-confirm', (), kwargs)
104
-
105
-    @models.permalink
106
-    def get_unsubscribe_url(self):
107
-        """
108
-        Get unsubscribe URL for this notification.
109
-        """
110
-        kwargs = self._get_url_kwargs()
111
-        kwargs['key'] = self.unsubscribe_key
112
-        return ('catalogue:notification-unsubscribe', (), kwargs)
113
-
114
-    @models.permalink
115
-    def get_absolute_item_url(self):
116
-        """
117
-        Get the absolute URL for the item referenced by this
118
-        notification. The URL patterns uses the ``item_url_index``
119
-        attribute specified in the class definition.
120
-        """
121
-        kwargs = self._get_url_kwargs()
122
-        return (self.product_url_index, (), kwargs)
123
-
124
-    def save(self, *args, **kwargs):
125
-        """
126
-        Save the current notification instance. If the user is not
127
-        a registered/authenticated user, a confirmation and
128
-        unsubscription key will be generated for the notification.
129
-        """
130
-        if self.email and not self.user:
131
-            if not self.confirm_key:
132
-                self.confirm_key = self.get_random_key()
133
-            if not self.unsubscribe_key:
134
-                self.unsubscribe_key = self.get_random_key()
135
-        return super(AbstractProductNotification, self).save(*args, **kwargs)
136
-
137
-    def _get_url_kwargs(self):
138
-        """
139
-        Get the keyword arguments for the confirmation URL reverse
140
-        lookup. This provides the item specific arguments for slug
141
-        and primary key.
142
-        """
143
-        return {
144
-            'product_slug': self.product.slug,
145
-            'pk': self.product.id,
146
-        }
147
-
148
-    def __unicode__(self):
149
-        """ Unicode representation of this notification """
150
-        return _(u'Notification for %(user)s - %(email)s') % {
151
-            'user': self.user or _("anonymous"),
152
-            'email': self.email
153
-        }
154
-
155
-    class Meta:
156
-        abstract = True
157
-        app_label = 'notification'

+ 0
- 38
oscar/apps/catalogue/notification/app.py Vedi File

@@ -1,38 +0,0 @@
1
-from django.db.models import get_model
2
-from django.conf.urls.defaults import patterns, url
3
-from django.contrib.auth.decorators import login_required
4
-
5
-from oscar.core.application import Application
6
-from oscar.apps.catalogue.notification import views
7
-
8
-ProductNotification = get_model('notification', 'productnotification')
9
-
10
-
11
-class ProductNotificationApplication(Application):
12
-    confirm_view = views.NotificationConfirmView
13
-    unsubscribe_view = views.NotificationUnsubscribeView
14
-    create_view = views.ProductNotificationCreateView
15
-    update_view = views.ProductNotificationSetStatusView
16
-
17
-    def get_urls(self):
18
-        urlpatterns = patterns('',
19
-            url(r'^confirm/(?P<key>[a-z0-9]{40})/$',
20
-                self.confirm_view.as_view(),
21
-                name='notification-confirm'),
22
-            url(r'^unsubscribe/(?P<key>[a-z0-9]{40})/$',
23
-                self.unsubscribe_view.as_view(),
24
-                name='notification-unsubscribe'),
25
-            url(r'^create/$', self.create_view.as_view(),
26
-                name='notification-create'),
27
-            # Make sure that only valid status values are allowed in the
28
-            # URL pattern
29
-            url(r'^(?P<notification_pk>\d+)/set-status/(?P<status>%s)/$' % (
30
-                    '|'.join([x[0] for x in ProductNotification.STATUS_TYPES])
31
-                ),
32
-                login_required(self.update_view.as_view()),
33
-                name='notification-set-status'),
34
-        )
35
-        return urlpatterns
36
-
37
-
38
-application = ProductNotificationApplication()

+ 0
- 25
oscar/apps/catalogue/notification/forms.py Vedi File

@@ -1,25 +0,0 @@
1
-from django import forms
2
-from django.utils.translation import ugettext_lazy as _
3
-
4
-
5
-class ProductNotificationForm(forms.Form):
6
-    """
7
-    Form providing a single email field for signing up to a notification. If
8
-    ``email`` or ``user`` are provided as initial values these values are
9
-    used to update the ``email`` field. If ``user`` is specified and the
10
-    user is registered and logged in, the ``email`` field is hidden in the
11
-    HTML template.
12
-    """
13
-    email = forms.EmailField(required=True, label=_(u'Send notification to'),
14
-                             widget=forms.TextInput(attrs={
15
-                                 'placeholder': _('Enter Your Email')
16
-                             }))
17
-
18
-    def __init__(self, *args, **kwargs):
19
-        super(ProductNotificationForm, self).__init__(*args, **kwargs)
20
-        user = self.initial.get('user', None)
21
-        if user and user.is_authenticated():
22
-            self.fields['email'].widget = forms.HiddenInput()
23
-
24
-            if not self.initial.get('email', None):
25
-                self.initial['email'] = user.email

+ 0
- 0
oscar/apps/catalogue/notification/migrations/__init__.py Vedi File


+ 0
- 8
oscar/apps/catalogue/notification/models.py Vedi File

@@ -1,8 +0,0 @@
1
-from oscar.apps.catalogue.notification import abstract_models
2
-
3
-
4
-class ProductNotification(abstract_models.AbstractProductNotification):
5
-    pass
6
-
7
-
8
-from oscar.apps.catalogue.notification.receivers import *

+ 0
- 23
oscar/apps/catalogue/notification/receivers.py Vedi File

@@ -1,23 +0,0 @@
1
-from django.conf import settings
2
-
3
-from oscar.core.loading import get_class
4
-from oscar.apps.catalogue.notification import utils
5
-
6
-# use get_class instead of get_model as this module get imported
7
-# in the models module of notification. That means models are not
8
-# available at this point in time.
9
-StockRecord = get_class('partner.models', 'StockRecord')
10
-
11
-
12
-def send_email_notifications(sender, instance, created, **kwargs):
13
-    """
14
-    Check for notifications for this product and send email to users
15
-    if the product is back in stock. Add a little 'hurry' note if the
16
-    amount of in-stock items is less then the number of notifications.
17
-    """
18
-    utils.send_email_notifications_for_product(instance.product)
19
-
20
-
21
-if settings.OSCAR_INSTANT_NOTIFICATION_ENABLED:
22
-    from django.db.models.signals import post_save
23
-    post_save.connect(send_email_notifications, sender=StockRecord)

+ 0
- 96
oscar/apps/catalogue/notification/utils.py Vedi File

@@ -1,96 +0,0 @@
1
-import datetime
2
-
3
-from django.core import mail
4
-from django.conf import settings
5
-from django.template import loader, Context
6
-from django.contrib.sites.models import Site
7
-from django.utils.translation import ugettext as _
8
-
9
-from oscar.core.loading import get_class
10
-
11
-# use get_class instead of get_model as this module get imported
12
-# in the models module of notification. That means models are not
13
-# available at this point in time.
14
-StockRecord = get_class('partner.models', 'StockRecord')
15
-ProductNotification = get_class('catalogue.notification.models',
16
-                                'ProductNotification')
17
-
18
-
19
-def create_email_from_context(email, template, context):
20
-    """
21
-    Create ``EmailMessage`` with message body composed as HTML from
22
-    *template* rendered with *context*. The email address to send the
23
-    message to is provided by *email*. The content subtype of the
24
-    message is set to ``html``.
25
-
26
-    Returns a ``EmailMessage`` instance.
27
-    """
28
-    subject = _("[Product Notification] Product '%(title)s' back in stock!")
29
-    subject = subject % {'title': context['product'].title}
30
-
31
-    msg = mail.EmailMessage(
32
-        template.render(context),
33
-        subject,
34
-        settings.OSCAR_FROM_EMAIL,
35
-        [email],
36
-    )
37
-    msg.content_subtype = "html",
38
-    return msg
39
-
40
-
41
-def send_email_notifications_for_product(product):
42
-    """
43
-    Check for notifications for this product and send email to users
44
-    if the product is back in stock. Add a little 'hurry' note if the
45
-    amount of in-stock items is less then the number of notifications.
46
-    """
47
-    notifications = ProductNotification.objects.filter(
48
-        product=product,
49
-        status=ProductNotification.ACTIVE,
50
-    )
51
-
52
-    # ignore the rest if no notifications for this stockrecord
53
-    if not len(notifications):
54
-        return
55
-
56
-    # add a hurry disclaimer if less products in stock then
57
-    # notifications requested
58
-    context = Context({
59
-        'product': product,
60
-        'site': Site.objects.get(pk=getattr(settings, 'SITE_ID', 1)),
61
-        'hurry': len(notifications) <= product.stockrecord.num_in_stock,
62
-    })
63
-
64
-    template_file = getattr(settings, 'OSCAR_NOTIFICATION_EMAIL_TEMPLATE',
65
-                            'notification/notification_email.html')
66
-    template = loader.get_template(template_file)
67
-
68
-    email_messages = []
69
-    # generate personalised emails for registered users first
70
-    for notification in notifications.exclude(user=None):
71
-        context['user'] = notification.user
72
-        email_messages.append(create_email_from_context(
73
-            notification.get_notification_email(),
74
-            template,
75
-            context
76
-        ))
77
-
78
-    # generate the same email for all anonymous users
79
-    for notification in notifications.filter(user=None):
80
-        email_messages.append(create_email_from_context(
81
-            notification.get_notification_email(),
82
-            template,
83
-            context
84
-        ))
85
-
86
-    # send all emails in one go to prevent multiple SMTP
87
-    # connections to be opened
88
-    connection = mail.get_connection()
89
-    connection.send_messages(email_messages)
90
-    connection.close()
91
-
92
-    # set the notified date for all notifications and deactivate them
93
-    for notification in notifications:
94
-        notification.status = ProductNotification.INACTIVE
95
-        notification.date_notified = datetime.datetime.now()
96
-        notification.save()

+ 0
- 295
oscar/apps/catalogue/notification/views.py Vedi File

@@ -1,295 +0,0 @@
1
-from django.conf import settings
2
-from django.views import generic
3
-from django.http import HttpResponseRedirect, Http404
4
-
5
-from django.core import mail
6
-from django.contrib import messages
7
-from django.db.models import get_model
8
-from django.template import loader, Context
9
-from django.contrib.auth.models import User
10
-from django.core.urlresolvers import reverse
11
-from django.contrib.sites.models import Site
12
-from django.shortcuts import get_object_or_404
13
-from django.utils.translation import ugettext as _
14
-
15
-from oscar.apps.catalogue.notification.forms import ProductNotificationForm
16
-
17
-Product = get_model('catalogue', 'product')
18
-ProductNotification = get_model('notification', 'productnotification')
19
-
20
-
21
-class NotificationUnsubscribeView(generic.RedirectView):
22
-    """
23
-    View to unsubscribe from a notification based on the provided
24
-    unsubscribe key. The notification is set to ``INACTIVE`` instead
25
-    of being deleted for analytical purposes.
26
-    """
27
-    model = ProductNotification
28
-    context_object_name = 'notification'
29
-
30
-    def get_object(self, queryset=None):
31
-        """ Get notification object that matches the unsubscribe key. """
32
-        try:
33
-            return self.model.objects.get(
34
-                unsubscribe_key=self.kwargs.get('key', 'invalid')
35
-            )
36
-        except self.model.DoesNotExist:
37
-            raise Http404
38
-
39
-    def get(self, *args, **kwargs):
40
-        notification = self.get_object()
41
-        notification.status = ProductNotification.INACTIVE
42
-        notification.save()
43
-        messages.info(self.request,
44
-            _("You have successfully unsubscribed from this notification.")
45
-        )
46
-        kwargs['notification'] = notification
47
-        return super(NotificationUnsubscribeView, self).get(*args, **kwargs)
48
-
49
-    def get_redirect_url(self, **kwargs):
50
-        return kwargs.get('notification').get_absolute_item_url()
51
-
52
-
53
-class NotificationConfirmView(generic.RedirectView):
54
-    """
55
-    View to confirm the email address of an anonymous user used to
56
-    sign up for a product notification.
57
-    """
58
-    model = ProductNotification
59
-    context_object_name = 'notification'
60
-
61
-    def get_object(self, queryset=None):
62
-        """ Get notification object that matches the confirmation key. """
63
-        try:
64
-            return self.model.objects.get(
65
-                confirm_key=self.kwargs.get('key', 'invalid')
66
-            )
67
-        except self.model.DoesNotExist:
68
-            raise Http404
69
-
70
-    def get(self, *args, **kwargs):
71
-        notification = self.get_object()
72
-        notification.status = self.model.ACTIVE
73
-        notification.save()
74
-        messages.info(self.request,
75
-            _("Yeah! You have confirmed your subscription. We'll notify "
76
-              "you as soon as the product is back in stock.")
77
-        )
78
-        kwargs['notification'] = notification
79
-        return super(NotificationConfirmView, self).get(*args, **kwargs)
80
-
81
-    def get_redirect_url(self, **kwargs):
82
-        return kwargs.get('notification').get_absolute_item_url()
83
-
84
-
85
-class ProductNotificationCreateView(generic.FormView):
86
-    """
87
-    View to create a new product notification based on a registered user
88
-    or an email address provided by an anonymous user.
89
-    """
90
-    product_model = Product
91
-    form_class = ProductNotificationForm
92
-    template_name = 'notification/notification.html'
93
-    email_template = 'notification/email.html'
94
-
95
-    def get_form_kwargs(self):
96
-        """
97
-        Get keywords to instantiate the view's form with. If the current
98
-        user is authenticated as registered user, their email address is
99
-        added to the ``initial`` dictionary as ``email`` which prepopulates
100
-        the ``email`` form field.
101
-        """
102
-        kwargs = super(ProductNotificationCreateView, self).get_form_kwargs()
103
-
104
-        user = self.request.user
105
-        if user.is_authenticated():
106
-            kwargs['initial'].update({'email': user.email})
107
-        return kwargs
108
-
109
-    def get_product(self):
110
-        """
111
-        Get product from primary key specified in URL patterns as
112
-        ``pk``. Raises a 404 if product does not exist.
113
-        """
114
-        return get_object_or_404(self.product_model, pk=self.kwargs['pk'])
115
-
116
-    def get_context_data(self, *args, **kwargs):
117
-        """
118
-        Get context data including ``product`` representing the product
119
-        related to this product notification.
120
-        """
121
-        ctx = super(ProductNotificationCreateView, self).get_context_data(
122
-            *args,
123
-            **kwargs
124
-        )
125
-        ctx['product'] = self.get_product()
126
-        return ctx
127
-
128
-    def get_notification_for_anonymous_user(self, email):
129
-        """
130
-        Get a the ``ProductNotification`` for the given product and anonymous
131
-        users email address. If no notification exists, a new
132
-        ``ProductNotification`` will be created for the product. A newly
133
-        created ``ProductNotification`` will be set to status ``UNCONFIRMED``
134
-        and need confirmation of the email address.
135
-        """
136
-        notification, created = ProductNotification.objects.get_or_create(
137
-            email=email,
138
-            product=self.product,
139
-        )
140
-
141
-        if created:
142
-            notification.status = ProductNotification.UNCONFIRMED
143
-            notification.save()
144
-        return notification, created
145
-
146
-    def get_notification_for_authenticated_user(self):
147
-        """
148
-        Get a the ``ProductNotification`` for the given product and anonymous
149
-        users email address. If no notification exists, a new
150
-        ``ProductNotification`` will be created for the product. A newly
151
-        created ``ProductNotification`` will be set to status ``ACTIVE``
152
-        immediately.
153
-        """
154
-        notification, created = ProductNotification.objects.get_or_create(
155
-            user=self.request.user,
156
-            product=self.product,
157
-        )
158
-
159
-        if created:
160
-            notification.status = ProductNotification.ACTIVE
161
-            notification.save()
162
-        return notification, created
163
-
164
-    def send_confirmation_email(self, notification):
165
-        """
166
-        Send email message to unregistered user's email address containing
167
-        a confirmation URL and a unsubscribe URL. This is the only means
168
-        for the user to active and deactivate their notification.
169
-        """
170
-        template = loader.get_template(self.email_template)
171
-        context = Context({
172
-            'site': Site.objects.get_current(),
173
-            'notification': notification,
174
-            'product': self.product,
175
-        })
176
-
177
-        msg = mail.EmailMessage(
178
-            _("[Confirmation] Please confirm the notification for "
179
-              "product %s") % (self.product.title,),
180
-            template.render(context),
181
-            settings.OSCAR_FROM_EMAIL,
182
-            [notification.email],
183
-        )
184
-        msg.send()
185
-        messages.info(self.request,
186
-                      _("Confirmation email send successfully, "
187
-                        "please check your email."))
188
-
189
-    def form_valid(self, form):
190
-        """
191
-        Handle creating a new ``ProductNotification`` when no errors
192
-        have been found in form. If a user has already signed up for
193
-        this ``ProductNotification``, no new notification will be
194
-        created but the user will receive confirmation that the notification
195
-        was created successfully. This reduces the complexity of handling
196
-        this situation differently from a new notification and the user
197
-        does not get annoyed by realising that they have already signed
198
-        up and just forgot about it.
199
-        If a new notification is created for an unregistered user, an
200
-        email is sent out with a confirmation and unsubscibe URL. A
201
-        registered users notification is activated immediately.
202
-
203
-        NOTE: a registered user that is not logged in will be redirected
204
-        to the login page. This is achieved by checking if an anonymous
205
-        email address is already part of a registered customer account.
206
-        """
207
-        is_authenticated = self.request.user.is_authenticated()
208
-        self.product = self.get_product()
209
-
210
-        # first check if the anonymous user provided an email address
211
-        # that belongs to a registered user. If that is the case the
212
-        # user will be redirected to the login/register page
213
-        if not is_authenticated:
214
-            try:
215
-                User.objects.get(email=form.cleaned_data['email'])
216
-                redirect_url = "%s?next=%s" % (reverse('customer:login'),
217
-                                               self.get_success_url())
218
-                return HttpResponseRedirect(redirect_url)
219
-            except User.DoesNotExist:
220
-                pass
221
-
222
-        if is_authenticated:
223
-            notification, created = self.get_notification_for_authenticated_user()
224
-        else:
225
-            notification, created = self.get_notification_for_anonymous_user(
226
-                form.cleaned_data['email']
227
-            )
228
-
229
-        if not created:
230
-            messages.success(self.request,
231
-                     _("you have signed up for a "
232
-                       "notification of '%s' already") % self.product.title)
233
-            return HttpResponseRedirect(self.get_success_url())
234
-
235
-        if created and not is_authenticated:
236
-            self.send_confirmation_email(notification)
237
-
238
-        messages.success(self.request,
239
-                _("%s was added to your notifications") % self.product.title)
240
-
241
-        return HttpResponseRedirect(self.get_success_url())
242
-
243
-    def get_success_url(self):
244
-        """
245
-        Get success URL to redirect to the product's detail page.
246
-        """
247
-        return reverse('catalogue:detail', kwargs={
248
-            'product_slug':self.product.slug,
249
-            'pk': self.product.pk
250
-        })
251
-
252
-
253
-class ProductNotificationSetStatusView(generic.TemplateView):
254
-    """
255
-    View to change the status of a product notification. The status can
256
-    be changed from ``active`` to ``inactive`` and vice versa.
257
-    """
258
-    model = ProductNotification
259
-    status_types = [map[0] for map in ProductNotification.STATUS_TYPES]
260
-    pk_url_kwarg = 'notification_pk'
261
-
262
-    def get(self, *args, **kwargs):
263
-        """
264
-        Handle GET request for this view. Extract the product and notification
265
-        from the URL as well as the new status. If the status is not ``active``
266
-        or ``inactive`` a HTTP redirect is returned without changing the
267
-        notification. Otherwise the notification status is updated
268
-        """
269
-        self.product = get_object_or_404(Product, pk=kwargs.get('pk'))
270
-        status = kwargs.get('status', None)
271
-
272
-        if status in (self.status_types):
273
-            notification = get_object_or_404(self.model, pk=kwargs.get('pk'))
274
-            notification.status = status
275
-            notification.save()
276
-
277
-        return HttpResponseRedirect(self.get_success_url())
278
-
279
-    def post(self, *args, **kwargs):
280
-        """
281
-        Handle POST request for this view similar to the GET request. Ignores
282
-        any POST request parameters. Extract the product and notification
283
-        from the URL as well as the new status. If the status is not ``active``
284
-        or ``inactive`` a HTTP redirect is returned without changing the
285
-        notification. Otherwise the notification status is updated
286
-        """
287
-        return self.get(*args, **kwargs)
288
-
289
-    def get_success_url(self):
290
-        """
291
-        Get URL to redirect to after successful status change.
292
-        """
293
-        detail_url = reverse('catalogue:detail',
294
-                             args=(self.product.slug, self.product.pk))
295
-        return self.request.META.get('HTTP_REFERER', detail_url)

+ 7
- 9
oscar/apps/catalogue/views.py Vedi File

@@ -9,8 +9,8 @@ from oscar.apps.catalogue.signals import product_viewed, product_search
9 9
 Product = get_model('catalogue', 'product')
10 10
 ProductReview = get_model('reviews', 'ProductReview')
11 11
 Category = get_model('catalogue', 'category')
12
-ProductNotificationForm = get_class('catalogue.notification.forms',
13
-                                    'ProductNotificationForm')
12
+ProductAlertForm = get_class('customer.forms',
13
+                             'ProductAlertForm')
14 14
 
15 15
 
16 16
 class ProductDetailView(DetailView):
@@ -29,12 +29,13 @@ class ProductDetailView(DetailView):
29 29
     def get_context_data(self, **kwargs):
30 30
         ctx = super(ProductDetailView, self).get_context_data(**kwargs)
31 31
         ctx['reviews'] = self.get_reviews()
32
-        ctx['notification_form'] = ProductNotificationForm(initial={
33
-            'user': self.request.user,
34
-            'email': getattr(self.request.user, 'email', ''),
35
-        })
32
+        ctx['alert_form'] = self.get_alert_form()
36 33
         return ctx
37 34
 
35
+    def get_alert_form(self):
36
+        return ProductAlertForm(user=self.request.user,
37
+                                product=self.object)
38
+
38 39
     def get_reviews(self):
39 40
         return self.object.reviews.filter(status=ProductReview.APPROVED)
40 41
 
@@ -153,7 +154,4 @@ class ProductListView(ListView):
153 154
         else:
154 155
             context['summary'] = _("Products matching '%(query)s'") % {'query': q}
155 156
             context['search_term'] = q
156
-        context['notification_form'] = ProductNotificationForm(initial={
157
-            'user':self.request.user,
158
-        })
159 157
         return context

+ 118
- 0
oscar/apps/customer/abstract_models.py Vedi File

@@ -1,8 +1,13 @@
1
+import sha
2
+import random
3
+
1 4
 from django.db import models
2 5
 from django.utils.translation import ugettext_lazy as _
3 6
 from django.template import Template, Context, TemplateDoesNotExist
4 7
 from django.template.loader import get_template
5 8
 from django.conf import settings
9
+from django.utils.timezone import now
10
+from django.core.urlresolvers import reverse
6 11
 
7 12
 from oscar.apps.customer.managers import CommunicationTypeManager
8 13
 
@@ -157,3 +162,116 @@ class AbstractNotification(models.Model):
157 162
     @property
158 163
     def is_read(self):
159 164
         return self.date_read is not None
165
+
166
+
167
+class AbstractProductAlert(models.Model):
168
+    """
169
+    An alert for when a product comes back in stock
170
+    """
171
+    product = models.ForeignKey('catalogue.Product')
172
+
173
+    # A user is only required if the notification is created by a
174
+    # registered user, anonymous users will only have an email address
175
+    # attached to the notification
176
+    user = models.ForeignKey('auth.User', db_index=True, blank=True, null=True,
177
+                             related_name="alerts", verbose_name=_('User'))
178
+    email = models.EmailField(_("Email"), db_index=True, blank=True, null=True)
179
+
180
+    # This key are used to confirm and cancel alerts for anon users
181
+    key = models.CharField(_("Key"), max_length=128, null=True, db_index=True)
182
+
183
+    # An alert can have two different statuses for authenticated
184
+    # users (``ACTIVE`` and ``INACTIVE`` and anonymous users have an
185
+    # additional status ``UNCONFIRMED``. For anonymous users a confirmation
186
+    # and unsubscription key are generated when an instance is saved for
187
+    # the first time and can be used to confirm and unsubscribe the
188
+    # notifications.
189
+    UNCONFIRMED, ACTIVE, CANCELLED, CLOSED = (
190
+        'Unconfirmed', 'Active', 'Cancelled', 'Closed')
191
+    STATUS_CHOICES = (
192
+        (UNCONFIRMED, _('Not yet confirmed')),
193
+        (ACTIVE, _('Active')),
194
+        (CANCELLED, _('Cancelled')),
195
+        (CLOSED, _('Closed')),
196
+    )
197
+    status = models.CharField(_("Status"), max_length=20,
198
+                              choices=STATUS_CHOICES, default=ACTIVE)
199
+
200
+    date_created = models.DateTimeField(_("Date created"), auto_now_add=True)
201
+    date_confirmed = models.DateTimeField(_("Date confirmed"), blank=True,
202
+                                          null=True)
203
+    date_cancelled = models.DateTimeField(_("Date cancelled"), blank=True,
204
+                                          null=True)
205
+    date_closed = models.DateTimeField(_("Date closed"), blank=True, null=True)
206
+
207
+    class Meta:
208
+        abstract = True
209
+
210
+    @property
211
+    def is_anonymous(self):
212
+        return self.user is None
213
+
214
+    @property
215
+    def can_be_confirmed(self):
216
+        return self.status == self.UNCONFIRMED
217
+
218
+    @property
219
+    def can_be_cancelled(self):
220
+        return self.status == self.ACTIVE
221
+
222
+    @property
223
+    def is_cancelled(self):
224
+        return self.status == self.CANCELLED
225
+
226
+    @property
227
+    def is_active(self):
228
+        return self.status == self.ACTIVE
229
+
230
+    def confirm(self):
231
+        self.status = self.ACTIVE
232
+        self.date_confirmed = now()
233
+        self.save()
234
+
235
+    def cancel(self):
236
+        self.status = self.CANCELLED
237
+        self.date_cancelled = now()
238
+        self.save()
239
+
240
+    def close(self):
241
+        self.status = self.CLOSED
242
+        self.date_closed = now()
243
+        self.save()
244
+
245
+    def get_email_address(self):
246
+        if self.user:
247
+            return self.user.email
248
+        else:
249
+            return self.email
250
+
251
+    def save(self, *args, **kwargs):
252
+        if not self.id and not self.user:
253
+            self.key = self.get_random_key()
254
+            self.status = self.UNCONFIRMED
255
+        # Ensure date fields get updated when saving from modelform (which just
256
+        # calls save, and doesn't call the methods cancel(), confirm() etc).
257
+        if self.status == self.CANCELLED and self.date_cancelled is None:
258
+            self.date_cancelled = now()
259
+        if not self.user and self.status == self.ACTIVE and self.date_confirmed is None:
260
+            self.date_confirmed = now()
261
+        if self.status == self.CLOSED and self.date_closed is None:
262
+            self.date_closed = now()
263
+
264
+        return super(AbstractProductAlert, self).save(*args, **kwargs)
265
+
266
+    def get_random_key(self):
267
+        """
268
+        Get a random generated key based on SHA-1 and email address
269
+        """
270
+        salt = sha.new(str(random.random())).hexdigest()
271
+        return sha.new(salt + self.email).hexdigest()
272
+
273
+    def get_confirm_url(self):
274
+        return reverse('customer:alerts-confirm', kwargs={'key': self.key})
275
+
276
+    def get_cancel_url(self):
277
+        return reverse('customer:alerts-cancel', kwargs={'key': self.key})

oscar/apps/catalogue/notification/__init__.py → oscar/apps/customer/alerts/__init__.py Vedi File


+ 28
- 0
oscar/apps/customer/alerts/receivers.py Vedi File

@@ -0,0 +1,28 @@
1
+from django.conf import settings
2
+from django.db.models import get_model
3
+from django.db.models.signals import post_save
4
+from django.contrib.auth.models import User
5
+
6
+
7
+def send_product_alerts(sender, instance, created, **kwargs):
8
+    from oscar.apps.customer.alerts import utils
9
+    utils.send_product_alerts(instance.product)
10
+
11
+
12
+def migrate_alerts_to_user(sender, instance, created, **kwargs):
13
+    """
14
+    Transfer any active alerts linked to a user's email address to the newly
15
+    registered user.
16
+    """
17
+    if created:
18
+        ProductAlert = get_model('customer', 'ProductAlert')
19
+        alerts = ProductAlert.objects.filter(email=instance.email, status=ProductAlert.ACTIVE)
20
+        alerts.update(user=instance, key=None, email=None)
21
+
22
+
23
+post_save.connect(migrate_alerts_to_user, sender=User)
24
+
25
+
26
+if settings.OSCAR_EAGER_ALERTS:
27
+    StockRecord = get_model('partner', 'StockRecord')
28
+    post_save.connect(send_product_alerts, sender=StockRecord)

+ 103
- 0
oscar/apps/customer/alerts/utils.py Vedi File

@@ -0,0 +1,103 @@
1
+import logging
2
+
3
+from django.core import mail
4
+from django.conf import settings
5
+from django.template import loader, Context
6
+from django.contrib.sites.models import Site
7
+from django.db.models import get_model
8
+
9
+from oscar.apps.customer.notifications import services
10
+
11
+ProductAlert = get_model('customer', 'ProductAlert')
12
+Product = get_model('catalogue', 'Product')
13
+
14
+logger = logging.getLogger(__file__)
15
+
16
+
17
+def send_alerts():
18
+    """
19
+    Send out product alerts
20
+    """
21
+    products = Product.objects.filter(
22
+        productalert__status=ProductAlert.ACTIVE
23
+    ).distinct()
24
+    logger.info("Found %d products with active alerts", products.count())
25
+    for product in products:
26
+        if product.is_available_to_buy:
27
+            send_product_alerts(product)
28
+
29
+
30
+def send_alert_confirmation(alert):
31
+    """
32
+    Send an alert confirmation email.
33
+    """
34
+    ctx = Context({
35
+        'alert': alert,
36
+        'site': Site.objects.get_current(),
37
+    })
38
+    subject_tpl = loader.get_template('customer/alerts/emails/confirmation_subject.txt')
39
+    body_tpl = loader.get_template('customer/alerts/emails/confirmation_body.txt')
40
+    mail.send_mail(
41
+        subject_tpl.render(ctx).strip(),
42
+        body_tpl.render(ctx),
43
+        settings.OSCAR_FROM_EMAIL,
44
+        [alert.email],
45
+    )
46
+
47
+
48
+def send_product_alerts(product):
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
+    if not product.has_stockrecord:
55
+        return
56
+    num_in_stock = product.stockrecord.num_in_stock
57
+    if num_in_stock == 0:
58
+        return
59
+
60
+    logger.info("Sending alerts for '%s'", product)
61
+    alerts = ProductAlert.objects.filter(
62
+        product=product,
63
+        status=ProductAlert.ACTIVE,
64
+    )
65
+    hurry_mode = alerts.count() < product.stockrecord.num_in_stock
66
+
67
+    # Load templates
68
+    message_tpl = loader.get_template('customer/alerts/message.html')
69
+    email_subject_tpl = loader.get_template('customer/alerts/emails/alert_subject.txt')
70
+    email_body_tpl = loader.get_template('customer/alerts/emails/alert_body.txt')
71
+
72
+    emails = []
73
+    num_notifications = 0
74
+    for alert in alerts:
75
+        ctx = Context({
76
+            'alert': alert,
77
+            'site': Site.objects.get_current(),
78
+            'hurry': hurry_mode,
79
+        })
80
+        if alert.user:
81
+            # Send a site notification
82
+            num_notifications += 1
83
+            services.notify_user(alert.user, message_tpl.render(ctx))
84
+
85
+        # Build email and add to list
86
+        emails.append(
87
+            mail.EmailMessage(
88
+                email_subject_tpl.render(ctx).strip(),
89
+                email_body_tpl.render(ctx),
90
+                settings.OSCAR_FROM_EMAIL,
91
+                [alert.get_email_address()],
92
+            )
93
+        )
94
+
95
+        alert.close()
96
+
97
+    # Send all emails in one go to prevent multiple SMTP
98
+    # connections to be opened
99
+    connection = mail.get_connection()
100
+    connection.send_messages(emails)
101
+    connection.close()
102
+
103
+    logger.info("Sent %d notifications and %d emails", num_notifications, len(emails))

+ 90
- 0
oscar/apps/customer/alerts/views.py Vedi File

@@ -0,0 +1,90 @@
1
+from django.views import generic
2
+from django.db.models import get_model
3
+from django.shortcuts import get_object_or_404
4
+from django.contrib import messages
5
+from django.utils.translation import ugettext_lazy as _
6
+from django import http
7
+
8
+from oscar.core.loading import get_class
9
+from oscar.apps.customer.alerts import utils
10
+
11
+Product = get_model('catalogue', 'Product')
12
+ProductAlert = get_model('customer', 'ProductAlert')
13
+ProductAlertForm = get_class('customer.forms', 'ProductAlertForm')
14
+
15
+
16
+class ProductAlertCreateView(generic.CreateView):
17
+    """
18
+    View to create a new product alert based on a registered user
19
+    or an email address provided by an anonymous user.
20
+    """
21
+    model = ProductAlert
22
+    form_class = ProductAlertForm
23
+    template_name = 'customer/alerts/form.html'
24
+
25
+    def get_context_data(self, **kwargs):
26
+        ctx = super(ProductAlertCreateView, self).get_context_data(**kwargs)
27
+        ctx['product'] = self.product
28
+        ctx['alert_form'] = ctx.pop('form')
29
+        return ctx
30
+
31
+    def get(self, request, *args, **kwargs):
32
+        product = get_object_or_404(Product, pk=self.kwargs['pk'])
33
+        return http.HttpResponseRedirect(product.get_absolute_url())
34
+
35
+    def post(self, request, *args, **kwargs):
36
+        self.product = get_object_or_404(Product, pk=self.kwargs['pk'])
37
+        return super(ProductAlertCreateView, self).post(request, *args, **kwargs)
38
+
39
+    def get_form_kwargs(self):
40
+        kwargs = super(ProductAlertCreateView, self).get_form_kwargs()
41
+        kwargs['user'] = self.request.user
42
+        kwargs['product'] = self.product
43
+        return kwargs
44
+
45
+    def form_valid(self, form):
46
+        response = super(ProductAlertCreateView, self).form_valid(form)
47
+        if self.object.is_anonymous:
48
+            utils.send_alert_confirmation(self.object)
49
+        return response
50
+
51
+    def get_success_url(self):
52
+        if self.object.user:
53
+            msg = _("An alert has been created")
54
+        else:
55
+            msg = _("An email has been sent to %s for confirmation") % self.object.email
56
+        messages.success(self.request, msg)
57
+        return self.object.product.get_absolute_url()
58
+
59
+
60
+class ProductAlertRedirectView(generic.RedirectView):
61
+    permanent = False
62
+
63
+    def get(self, request, *args, **kwargs):
64
+        self.alert = get_object_or_404(ProductAlert, key=kwargs['key'])
65
+        self.update_alert()
66
+        return super(ProductAlertRedirectView, self).get(request, *args, **kwargs)
67
+
68
+    def get_redirect_url(self, **kwargs):
69
+        return self.alert.product.get_absolute_url()
70
+
71
+
72
+class ProductAlertConfirmView(ProductAlertRedirectView):
73
+    permanent = False
74
+
75
+    def update_alert(self):
76
+        if self.alert.can_be_confirmed:
77
+            self.alert.confirm()
78
+            messages.success(self.request, _("Your stock alert is now active"))
79
+        else:
80
+            messages.error(self.request, _("Your stock alert cannot be confirmed"))
81
+
82
+
83
+class ProductAlertCancelView(ProductAlertRedirectView):
84
+
85
+    def update_alert(self):
86
+        if self.alert.can_be_cancelled:
87
+            self.alert.cancel()
88
+            messages.success(self.request, _("Your stock alert has been cancelled"))
89
+        else:
90
+            messages.error(self.request, _("Your stock alert cannot be cancelled"))

+ 15
- 0
oscar/apps/customer/app.py Vedi File

@@ -4,6 +4,7 @@ from django.views import generic
4 4
 
5 5
 from oscar.apps.customer import views
6 6
 from oscar.apps.customer.notifications import views as notification_views
7
+from oscar.apps.customer.alerts import views as alert_views
7 8
 from oscar.core.application import Application
8 9
 
9 10
 
@@ -30,6 +31,10 @@ class CustomerApplication(Application):
30 31
     notification_archive_view = notification_views.ArchiveView
31 32
     notification_update_view = notification_views.UpdateView
32 33
 
34
+    alert_create_view = alert_views.ProductAlertCreateView
35
+    alert_confirm_view = alert_views.ProductAlertConfirmView
36
+    alert_cancel_view = alert_views.ProductAlertCancelView
37
+
33 38
     def get_urls(self):
34 39
         urlpatterns = patterns('',
35 40
             url(r'^$', login_required(self.summary_view.as_view()),
@@ -92,6 +97,16 @@ class CustomerApplication(Application):
92 97
             url(r'^notifications/update/$',
93 98
                 login_required(self.notification_update_view.as_view()),
94 99
                 name='notifications-update'),
100
+
101
+            # Alerts
102
+            url(r'^alerts/create/(?P<pk>\d+)/$', self.alert_create_view.as_view(),
103
+                name='alert-create'),
104
+            url(r'^alerts/confirm/(?P<key>[a-z0-9]+)/$',
105
+                self.alert_confirm_view.as_view(),
106
+                name='alerts-confirm'),
107
+            url(r'^alerts/cancel/(?P<key>[a-z0-9]+)/$',
108
+                self.alert_cancel_view.as_view(),
109
+                name='alerts-cancel'),
95 110
             )
96 111
         return self.post_process_urls(urlpatterns)
97 112
 

+ 58
- 0
oscar/apps/customer/forms.py Vedi File

@@ -18,6 +18,7 @@ from oscar.core.loading import get_profile_class, get_class
18 18
 
19 19
 Dispatcher = get_class('customer.utils', 'Dispatcher')
20 20
 CommunicationEventType = get_model('customer', 'communicationeventtype')
21
+ProductAlert = get_model('customer', 'ProductAlert')
21 22
 
22 23
 
23 24
 def generate_username():
@@ -275,3 +276,60 @@ if hasattr(settings, 'AUTH_PROFILE_MODULE'):
275 276
     ProfileForm = UserAndProfileForm
276 277
 else:
277 278
     ProfileForm = UserForm
279
+
280
+
281
+class ProductAlertForm(forms.ModelForm):
282
+    email = forms.EmailField(required=True, label=_(u'Send notification to'),
283
+                             widget=forms.TextInput(attrs={
284
+                                 'placeholder': _('Enter your email')
285
+                             }))
286
+
287
+    def __init__(self, user, product, *args, **kwargs):
288
+        self.user = user
289
+        self.product = product
290
+        super(ProductAlertForm, self).__init__(*args, **kwargs)
291
+
292
+        # Only show email field to unauthenticated users
293
+        if user and user.is_authenticated():
294
+            self.fields['email'].widget = forms.HiddenInput()
295
+            self.fields['email'].required = False
296
+
297
+    def save(self, commit=True):
298
+        alert = super(ProductAlertForm, self).save(commit=False)
299
+        if self.user.is_authenticated():
300
+            alert.user = self.user
301
+        alert.product = self.product
302
+        if commit:
303
+            alert.save()
304
+        return alert
305
+
306
+    def clean(self):
307
+        cleaned_data = self.cleaned_data
308
+        email = cleaned_data.get('email')
309
+        if email:
310
+            try:
311
+                ProductAlert.objects.get(
312
+                    product=self.product, email=email,
313
+                    status=ProductAlert.ACTIVE)
314
+            except ProductAlert.DoesNotExist:
315
+                pass
316
+            else:
317
+                raise forms.ValidationError(_(
318
+                    "There is already an active stock alert for %s") % email)
319
+        elif self.user.is_authenticated():
320
+            try:
321
+                ProductAlert.objects.get(product=self.product,
322
+                                         user=self.user,
323
+                                         status=ProductAlert.ACTIVE)
324
+            except ProductAlert.DoesNotExist:
325
+                pass
326
+            else:
327
+                raise forms.ValidationError(_(
328
+                    "You already have an active alert for this product"))
329
+        return cleaned_data
330
+
331
+    class Meta:
332
+        model = ProductAlert
333
+        exclude = ('user', 'key',
334
+                   'status', 'date_confirmed', 'date_cancelled', 'date_closed',
335
+                   'product')

oscar/apps/catalogue/notification/migrations/0001_initial.py → oscar/apps/customer/migrations/0003_auto__add_productalert.py Vedi File

@@ -1,32 +1,33 @@
1
-# -*- coding: utf-8 -*-
1
+# encoding: utf-8
2 2
 import datetime
3 3
 from south.db import db
4 4
 from south.v2 import SchemaMigration
5 5
 from django.db import models
6 6
 
7
-
8 7
 class Migration(SchemaMigration):
9 8
 
10 9
     def forwards(self, orm):
11
-        # Adding model 'ProductNotification'
12
-        db.create_table('notification_productnotification', (
10
+
11
+        # Adding model 'ProductAlert'
12
+        db.create_table('customer_productalert', (
13 13
             ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14
-            ('user', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='product_notifications', null=True, to=orm['auth.User'])),
15 14
             ('product', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Product'])),
15
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='alerts', null=True, to=orm['auth.User'])),
16 16
             ('email', self.gf('django.db.models.fields.EmailField')(db_index=True, max_length=75, null=True, blank=True)),
17
-            ('confirm_key', self.gf('django.db.models.fields.CharField')(max_length=40, null=True)),
18
-            ('unsubscribe_key', self.gf('django.db.models.fields.CharField')(max_length=40, null=True)),
19
-            ('status', self.gf('django.db.models.fields.CharField')(default='inactive', max_length=20)),
17
+            ('key', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, db_index=True)),
18
+            ('status', self.gf('django.db.models.fields.CharField')(default='Active', max_length=20)),
20 19
             ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
21
-            ('date_modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
22
-            ('date_notified', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
20
+            ('date_confirmed', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
21
+            ('date_cancelled', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
22
+            ('date_closed', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
23 23
         ))
24
-        db.send_create_signal('notification', ['ProductNotification'])
24
+        db.send_create_signal('customer', ['ProductAlert'])
25 25
 
26 26
 
27 27
     def backwards(self, orm):
28
-        # Deleting model 'ProductNotification'
29
-        db.delete_table('notification_productnotification')
28
+
29
+        # Deleting model 'ProductAlert'
30
+        db.delete_table('customer_productalert')
30 31
 
31 32
 
32 33
     models = {
@@ -45,7 +46,7 @@ class Migration(SchemaMigration):
45 46
         },
46 47
         'auth.user': {
47 48
             'Meta': {'object_name': 'User'},
48
-            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
49
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 9, 26, 13, 49, 39, 401244)'}),
49 50
             'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
50 51
             'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
51 52
             'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
@@ -53,7 +54,7 @@ class Migration(SchemaMigration):
53 54
             'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
54 55
             'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
55 56
             'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
56
-            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
57
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 9, 26, 13, 49, 39, 401151)'}),
57 58
             'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
58 59
             'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
59 60
             'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
@@ -63,14 +64,14 @@ class Migration(SchemaMigration):
63 64
             'Meta': {'object_name': 'AttributeEntity'},
64 65
             'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
65 66
             'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
66
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'blank': 'True'}),
67
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
67 68
             'type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entities'", 'to': "orm['catalogue.AttributeEntityType']"})
68 69
         },
69 70
         'catalogue.attributeentitytype': {
70 71
             'Meta': {'object_name': 'AttributeEntityType'},
71 72
             'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
72 73
             'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
73
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'blank': 'True'})
74
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'})
74 75
         },
75 76
         'catalogue.attributeoption': {
76 77
             'Meta': {'object_name': 'AttributeOption'},
@@ -93,11 +94,11 @@ class Migration(SchemaMigration):
93 94
             'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
94 95
             'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
95 96
             'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
96
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '1024'})
97
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '1024', 'db_index': 'True'})
97 98
         },
98 99
         'catalogue.option': {
99 100
             'Meta': {'object_name': 'Option'},
100
-            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128'}),
101
+            'code': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
101 102
             'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
102 103
             'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
103 104
             'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
@@ -117,14 +118,14 @@ class Migration(SchemaMigration):
117 118
             'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Product']", 'symmetrical': 'False', 'through': "orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
118 119
             'related_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'relations'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
119 120
             'score': ('django.db.models.fields.FloatField', [], {'default': '0.0', 'db_index': 'True'}),
120
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
121
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
121 122
             'status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '128', 'null': 'True', 'blank': 'True'}),
122 123
             'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
123 124
             'upc': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
124 125
         },
125 126
         'catalogue.productattribute': {
126 127
             'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
127
-            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128'}),
128
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
128 129
             'entity_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntityType']", 'null': 'True', 'blank': 'True'}),
129 130
             'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
130 131
             'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
@@ -160,7 +161,8 @@ class Migration(SchemaMigration):
160 161
             'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
161 162
             'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
162 163
             'requires_shipping': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
163
-            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128'})
164
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
165
+            'track_stock': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
164 166
         },
165 167
         'catalogue.productrecommendation': {
166 168
             'Meta': {'object_name': 'ProductRecommendation'},
@@ -176,19 +178,53 @@ class Migration(SchemaMigration):
176 178
             'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
177 179
             'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
178 180
         },
179
-        'notification.productnotification': {
180
-            'Meta': {'object_name': 'ProductNotification'},
181
-            'confirm_key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True'}),
181
+        'customer.communicationeventtype': {
182
+            'Meta': {'object_name': 'CommunicationEventType'},
183
+            'category': ('django.db.models.fields.CharField', [], {'default': "u'Order related'", 'max_length': '255'}),
184
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
185
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
186
+            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
187
+            'email_body_html_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
188
+            'email_body_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
189
+            'email_subject_template': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
190
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
191
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
192
+            'sms_template': ('django.db.models.fields.CharField', [], {'max_length': '170', 'blank': 'True'})
193
+        },
194
+        'customer.email': {
195
+            'Meta': {'object_name': 'Email'},
196
+            'body_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
197
+            'body_text': ('django.db.models.fields.TextField', [], {}),
198
+            'date_sent': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
199
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
200
+            'subject': ('django.db.models.fields.TextField', [], {'max_length': '255'}),
201
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emails'", 'to': "orm['auth.User']"})
202
+        },
203
+        'customer.notification': {
204
+            'Meta': {'ordering': "('-date_sent',)", 'object_name': 'Notification'},
205
+            'body': ('django.db.models.fields.TextField', [], {}),
206
+            'category': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
207
+            'date_read': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
208
+            'date_sent': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
209
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
210
+            'location': ('django.db.models.fields.CharField', [], {'default': "'Inbox'", 'max_length': '32'}),
211
+            'recipient': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notifications'", 'to': "orm['auth.User']"}),
212
+            'sender': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
213
+            'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'})
214
+        },
215
+        'customer.productalert': {
216
+            'Meta': {'object_name': 'ProductAlert'},
217
+            'date_cancelled': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
218
+            'date_closed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
219
+            'date_confirmed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
182 220
             'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
183
-            'date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
184
-            'date_notified': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
185 221
             'email': ('django.db.models.fields.EmailField', [], {'db_index': 'True', 'max_length': '75', 'null': 'True', 'blank': 'True'}),
186 222
             'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
223
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}),
187 224
             'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"}),
188
-            'status': ('django.db.models.fields.CharField', [], {'default': "'inactive'", 'max_length': '20'}),
189
-            'unsubscribe_key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True'}),
190
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'product_notifications'", 'null': 'True', 'to': "orm['auth.User']"})
225
+            'status': ('django.db.models.fields.CharField', [], {'default': "'Active'", 'max_length': '20'}),
226
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'alerts'", 'null': 'True', 'to': "orm['auth.User']"})
191 227
         }
192 228
     }
193 229
 
194
-    complete_apps = ['notification']
230
+    complete_apps = ['customer']

+ 10
- 6
oscar/apps/customer/models.py Vedi File

@@ -1,17 +1,21 @@
1
-from oscar.apps.customer.abstract_models import (
2
-    AbstractEmail, AbstractCommunicationEventType, AbstractNotification)
1
+from oscar.apps.customer import abstract_models
3 2
 
4 3
 
5
-class Email(AbstractEmail):
4
+class Email(abstract_models.AbstractEmail):
6 5
     pass
7 6
 
8 7
 
9
-class CommunicationEventType(AbstractCommunicationEventType):
8
+class CommunicationEventType(abstract_models.AbstractCommunicationEventType):
10 9
     pass
11 10
 
12 11
 
13
-class Notification(AbstractNotification):
12
+class Notification(abstract_models.AbstractNotification):
14 13
     pass
15 14
 
16 15
 
17
-from oscar.apps.customer.history_helpers import *
16
+class ProductAlert(abstract_models.AbstractProductAlert):
17
+    pass
18
+
19
+
20
+from oscar.apps.customer.history_helpers import *
21
+from oscar.apps.customer.alerts.receivers import *

+ 16
- 39
oscar/apps/customer/views.py Vedi File

@@ -33,7 +33,7 @@ UserAddress = get_model('address', 'UserAddress')
33 33
 Email = get_model('customer', 'email')
34 34
 UserAddress = get_model('address', 'UserAddress')
35 35
 CommunicationEventType = get_model('customer', 'communicationeventtype')
36
-ProductNotification = get_model('notification', 'productnotification')
36
+ProductAlert = get_model('customer', 'ProductAlert')
37 37
 
38 38
 
39 39
 class LogoutView(RedirectView):
@@ -79,7 +79,7 @@ class AccountSummaryView(TemplateView):
79 79
         ctx['default_billing_address'] = self.get_default_billing_address(self.request.user)
80 80
         ctx['orders'] = self.get_orders(self.request.user)
81 81
         ctx['emails'] = self.get_emails(self.request.user)
82
-        ctx['notification_list'] = self.get_product_notifications(self.request.user)
82
+        ctx['alerts'] = self.get_product_alerts(self.request.user)
83 83
         self.add_profile_fields(ctx)
84 84
 
85 85
         ctx['active_tab'] = self.request.GET.get('tab', 'profile')
@@ -113,38 +113,30 @@ class AccountSummaryView(TemplateView):
113 113
         ctx['profile'] = profile
114 114
 
115 115
     def post(self, request, *args, **kwargs):
116
-        if 'deactivate' in request.POST:
117
-            notification_id = request.POST.get('deactivate')
118
-            status = ProductNotification.INACTIVE
119
-            success_msg = _("Notification deactivated")
120
-        elif 'activate' in request.POST:
121
-            notification_id = request.POST.get('activate')
122
-            status = ProductNotification.ACTIVE
123
-            success_msg = _("Notification activated")
116
+        # A POST means an attempt to change the status of an alert
117
+        if 'cancel_alert' in request.POST:
118
+            return self.cancel_alert(request.POST.get('cancel_alert'))
119
+        return super(AccountSummaryView, self).post(request, *args, **kwargs)
124 120
 
121
+    def cancel_alert(self, alert_id):
125 122
         try:
126
-            notification = ProductNotification.objects.get(pk=notification_id,
127
-                                                           user=request.user)
128
-        except ProductNotification.DoesNotExist:
129
-            messages.error(_("Cannot change notification status, notification "
130
-                             "does not exist"))
123
+            alert = ProductAlert.objects.get(user=self.request.user, pk=alert_id)
124
+        except ProductAlert.DoesNotExist:
125
+            messages.error(self.request, _("No alert found"))
131 126
         else:
132
-            messages.success(request, success_msg)
133
-            notification.status = status
134
-            notification.save()
135
-
127
+            alert.cancel()
128
+            messages.success(self.request, _("Alert cancelled"))
136 129
         return HttpResponseRedirect(
137
-            reverse('customer:summary')+'?tab=notifications'
130
+            reverse('customer:summary')+'?tab=alerts'
138 131
         )
139 132
 
140 133
     def get_emails(self, user):
141 134
         return Email.objects.filter(user=user)
142 135
 
143
-    def get_product_notifications(self, user):
144
-        # Only show notifications that have not been processed
145
-        return ProductNotification.objects.select_related().filter(
136
+    def get_product_alerts(self, user):
137
+        return ProductAlert.objects.select_related().filter(
146 138
             user=self.request.user,
147
-            date_notified=None,
139
+            date_closed=None,
148 140
         )
149 141
 
150 142
     def get_default_billing_address(self, user):
@@ -238,10 +230,6 @@ class AccountRegistrationView(TemplateView):
238 230
         ``OSCAR_SEND_REGISTRATION_EMAIL`` is set to ``True`` a
239 231
         registration email will be send to the provided email address.
240 232
         A new user account is created and the user is then logged in.
241
-
242
-        If the user has anonymous ``Notifications`` subscriptions
243
-        from before they registered, these notifications will be updated
244
-        to be connected with their newly created user account.
245 233
         """
246 234
         user = form.save()
247 235
 
@@ -269,17 +257,6 @@ class AccountRegistrationView(TemplateView):
269 257
         if self.request.session.test_cookie_worked():
270 258
             self.request.session.delete_test_cookie()
271 259
 
272
-        # check if there are notifications for this user's
273
-        # email address and change them from anonymous to this
274
-        # user's account
275
-        #FIXME: Needs to implement base class of all notifications
276
-        notifications = ProductNotification.objects.filter(
277
-            email=user.email,
278
-            user=None
279
-        )
280
-        for notification in notifications:
281
-            notification.transfer_to_user(user)
282
-
283 260
 
284 261
 class AccountAuthView(AccountRegistrationView):
285 262
     template_name = 'customer/login_registration.html'

+ 17
- 13
oscar/apps/dashboard/users/app.py Vedi File

@@ -8,7 +8,7 @@ from oscar.apps.dashboard.nav import register, Node
8 8
 
9 9
 node = Node(_('Customers'))
10 10
 node.add_child(Node(_('Customers'), 'dashboard:users-index'))
11
-node.add_child(Node(_('Notifications'), 'dashboard:user-notification-list'))
11
+node.add_child(Node(_('Alerts'), 'dashboard:user-alert-list'))
12 12
 register(node, 30)
13 13
 
14 14
 
@@ -16,24 +16,28 @@ class UserManagementApplication(Application):
16 16
     name = None
17 17
     index_view = views.IndexView
18 18
     user_detail_view = views.UserDetailView
19
-    notification_list_view = views.ProductNotificationListView
20
-    notification_update_view = views.ProductNotificationUpdateView
21
-    notification_delete_view = views.ProductNotificationDeleteView
19
+    alert_list_view = views.ProductAlertListView
20
+    alert_update_view = views.ProductAlertUpdateView
21
+    alert_delete_view = views.ProductAlertDeleteView
22 22
 
23 23
     def get_urls(self):
24 24
         urlpatterns = patterns('',
25 25
             url(r'^$', self.index_view.as_view(), name='users-index'),
26
-            url(r'^notifications/(?P<pk>\d+)/delete/$',
27
-                self.notification_delete_view.as_view(),
28
-                name='user-notification-delete'),
29
-            url(r'^notifications/(?P<pk>\d+)/update/$',
30
-                self.notification_update_view.as_view(),
31
-                name='user-notification-update'),
32
-            url(r'^notifications/$',
33
-                self.notification_list_view.as_view(),
34
-                name='user-notification-list'),
26
+
27
+            # Alerts
28
+            url(r'^alerts/(?P<pk>\d+)/delete/$',
29
+                self.alert_delete_view.as_view(),
30
+                name='user-alert-delete'),
31
+            url(r'^alerts/(?P<pk>\d+)/update/$',
32
+                self.alert_update_view.as_view(),
33
+                name='user-alert-update'),
34
+            url(r'^alerts/$',
35
+                self.alert_list_view.as_view(),
36
+                name='user-alert-list'),
37
+
35 38
             url(r'^(?P<pk>[-\w]+)/$',
36 39
                 self.user_detail_view.as_view(), name='user-detail'),
40
+
37 41
         )
38 42
         return self.post_process_urls(urlpatterns)
39 43
 

+ 18
- 6
oscar/apps/dashboard/users/forms.py Vedi File

@@ -3,7 +3,7 @@ from django.db.models.loading import get_model
3 3
 from django.utils.translation import ugettext_lazy as _
4 4
 
5 5
 User = get_model('user', 'User')
6
-ProductNotification = get_model('notification', 'productnotification')
6
+ProductAlert = get_model('customer', 'ProductAlert')
7 7
 
8 8
 
9 9
 class UserSearchForm(forms.Form):
@@ -11,16 +11,28 @@ class UserSearchForm(forms.Form):
11 11
     name = forms.CharField(required=False, label=_("Name"))
12 12
 
13 13
 
14
-class ProductNotificationUpdateForm(forms.ModelForm):
14
+class ProductAlertUpdateForm(forms.ModelForm):
15
+
16
+    def __init__(self, *args, **kwargs):
17
+        super(ProductAlertUpdateForm, self).__init__(*args, **kwargs)
18
+        alert = kwargs['instance']
19
+        if alert.user:
20
+            # Remove 'unconfirmed' from list of available choices when editing
21
+            # an alert for a real user
22
+            choices = self.fields['status'].choices
23
+            del choices[0]
24
+            self.fields['status'].choices = choices
25
+
15 26
     class Meta:
16
-        model = ProductNotification
17
-        exclude = ('confirm_key', 'unsubscribe_key')
27
+        model = ProductAlert
28
+        exclude = ('product', 'user', 'email', 'key',
29
+                   'date_confirmed', 'date_cancelled', 'date_closed')
18 30
 
19 31
 
20
-class ProductNotificationSearchForm(forms.Form):
32
+class ProductAlertSearchForm(forms.Form):
21 33
     STATUS_CHOICES = (
22 34
         ('', '------------'),
23
-    ) + ProductNotification.STATUS_TYPES
35
+    ) + ProductAlert.STATUS_CHOICES
24 36
 
25 37
     status = forms.ChoiceField(required=False, choices=STATUS_CHOICES)
26 38
     name = forms.CharField(required=False)

+ 27
- 32
oscar/apps/dashboard/users/views.py Vedi File

@@ -6,10 +6,13 @@ from django.http import HttpResponseRedirect
6 6
 from django.core.urlresolvers import reverse
7 7
 from django.views.generic import ListView, DetailView, DeleteView, UpdateView
8 8
 
9
-from oscar.apps.dashboard.users import forms
10 9
 from oscar.views.generic import BulkEditMixin
10
+from oscar.core.loading import get_classes
11 11
 
12
-ProductNotification = get_model('notification', 'productnotification')
12
+UserSearchForm, ProductAlertSearchForm, ProductAlertUpdateForm  = get_classes(
13
+    'dashboard.users.forms', ('UserSearchForm', 'ProductAlertSearchForm',
14
+                              'ProductAlertUpdateForm'))
15
+ProductAlert = get_model('customer', 'ProductAlert')
13 16
 
14 17
 
15 18
 class IndexView(ListView, BulkEditMixin):
@@ -18,7 +21,7 @@ class IndexView(ListView, BulkEditMixin):
18 21
     model = User
19 22
     actions = ('make_active', 'make_inactive', )
20 23
     current_view = 'dashboard:users-index'
21
-    form_class = forms.UserSearchForm
24
+    form_class = UserSearchForm
22 25
     desc_template = _('%(main_filter)s %(email_filter)s %(name_filter)s')
23 26
     description = ''
24 27
 
@@ -88,15 +91,13 @@ class UserDetailView(DetailView):
88 91
         return context
89 92
 
90 93
 
91
-class ProductNotificationListView(ListView, BulkEditMixin):
92
-    model = ProductNotification
93
-    form_class = forms.ProductNotificationSearchForm
94
-    context_object_name = 'notification_list'
95
-    template_name = 'dashboard/notification/list.html'
96
-    paginate = 25
97
-    actions = ('update_selected_notification_status',)
98
-    base_description = _('All notifications')
99
-    checkbox_object_name = 'notification'
94
+class ProductAlertListView(ListView):
95
+    model = ProductAlert
96
+    form_class = ProductAlertSearchForm
97
+    context_object_name = 'alerts'
98
+    template_name = 'dashboard/users/alerts/list.html'
99
+    paginate_by = 20
100
+    base_description = _('All Alerts')
100 101
     description = ''
101 102
 
102 103
     def get_queryset(self):
@@ -138,35 +139,29 @@ class ProductNotificationListView(ListView, BulkEditMixin):
138 139
 
139 140
         return queryset
140 141
 
141
-    def update_selected_notification_status(self, request, notifications):
142
-        new_status = request.POST.get('status')
143
-        for notification in notifications:
144
-            notification.status = new_status
145
-            notification.save()
146
-        return HttpResponseRedirect(reverse('dashboard:user-notification-list'))
147
-
148 142
     def get_context_data(self, **kwargs):
149
-        context = super(ProductNotificationListView, self).get_context_data(**kwargs)
143
+        context = super(ProductAlertListView, self).get_context_data(**kwargs)
150 144
         context['form'] = self.form
151
-        context['notification_form'] = forms.ProductNotificationUpdateForm
152 145
         context['queryset_description'] = self.description
153 146
         return context
154 147
 
155 148
 
156
-class ProductNotificationUpdateView(UpdateView):
157
-    template_name = 'dashboard/notification/update.html'
158
-    model = ProductNotification
159
-    form_class = forms.ProductNotificationUpdateForm
160
-    context_object_name = 'notification'
149
+class ProductAlertUpdateView(UpdateView):
150
+    template_name = 'dashboard/users/alerts/update.html'
151
+    model = ProductAlert
152
+    form_class = ProductAlertUpdateForm
153
+    context_object_name = 'alert'
161 154
 
162 155
     def get_success_url(self):
163
-        return reverse('dashboard:user-notification-list')
156
+        messages.success(self.request, _("Product alert saved"))
157
+        return reverse('dashboard:user-alert-list')
164 158
 
165 159
 
166
-class ProductNotificationDeleteView(DeleteView):
167
-    model = ProductNotification
168
-    template_name = 'dashboard/notification/delete.html'
169
-    context_object_name = 'notification'
160
+class ProductAlertDeleteView(DeleteView):
161
+    model = ProductAlert
162
+    template_name = 'dashboard/users/alerts/delete.html'
163
+    context_object_name = 'alert'
170 164
 
171 165
     def get_success_url(self):
172
-        return reverse('dashboard:user-notification-list')
166
+        messages.warning(self.request, _("Product alert deleted"))
167
+        return reverse('dashboard:user-alert-list')

+ 4
- 6
oscar/defaults.py Vedi File

@@ -44,18 +44,16 @@ OSCAR_PROMOTION_MERCHANDISING_BLOCK_TYPES = (
44 44
 OSCAR_ALLOW_ANON_REVIEWS = True
45 45
 OSCAR_MODERATE_REVIEWS = False
46 46
 
47
-# Notifications
48
-OSCAR_NOTIFICATION_EMAIL_TEMPLATE = 'notification/notification_email.html'
49
-# This enables sending notification emails
47
+# This enables sending alert notifications/emails
50 48
 # instantly when products get back in stock
51 49
 # by listening to stock record update signals
52 50
 # this might impact performace for large numbers
53 51
 # stock record updates.
54 52
 # Alternatively, the management command
55
-# ``oscar_send_notifications`` can be used to
53
+# ``oscar_send_alerts`` can be used to
56 54
 # run periodically, e.g. as a cronjob. In this case
57
-# instant notifications should be disabled.
58
-OSCAR_INSTANT_NOTIFICATION_ENABLED = True
55
+# instant alerts should be disabled.
56
+OSCAR_EAGER_ALERTS = True
59 57
 
60 58
 # Registration
61 59
 OSCAR_SEND_REGISTRATION_EMAIL = True

+ 1859
- 1518
oscar/locale/da/LC_MESSAGES/django.po
File diff soppresso perché troppo grande
Vedi File


+ 1859
- 1518
oscar/locale/de/LC_MESSAGES/django.po
File diff soppresso perché troppo grande
Vedi File


+ 1859
- 1518
oscar/locale/es/LC_MESSAGES/django.po
File diff soppresso perché troppo grande
Vedi File


+ 1859
- 1518
oscar/locale/fr/LC_MESSAGES/django.po
File diff soppresso perché troppo grande
Vedi File


+ 1859
- 1518
oscar/locale/it/LC_MESSAGES/django.po
File diff soppresso perché troppo grande
Vedi File


+ 1863
- 1517
oscar/locale/pl/LC_MESSAGES/django.po
File diff soppresso perché troppo grande
Vedi File


+ 1863
- 1517
oscar/locale/ru/LC_MESSAGES/django.po
File diff soppresso perché troppo grande
Vedi File


oscar/management/commands/oscar_cleanup_notifications.py → oscar/management/commands/oscar_cleanup_alerts.py Vedi File

@@ -1,56 +1,54 @@
1 1
 import logging
2
-
3 2
 from optparse import make_option
4
-from datetime import datetime, timedelta
3
+from datetime import timedelta
5 4
 
6 5
 from django.db.models import get_model
6
+from django.utils.timezone import now
7 7
 from django.core.management.base import BaseCommand
8 8
 
9
-ProductNotification = get_model('notification', 'productnotification')
9
+ProductAlert = get_model('customer', 'ProductAlert')
10 10
 
11 11
 logger = logging.getLogger(__name__)
12 12
 
13 13
 
14 14
 class Command(BaseCommand):
15 15
     """
16
-    Command to remove all notifications derived from
17
-    ``Notification`` that are in status ``UNCONFIRMED`` and
18
-    have been created before a threshold date and time. The threshold
19
-    can be specified as options ``days`` and ``hours`` and is
20
-    calculated relative to the current date and time.
16
+    Command to remove all stale unconfirmed alerts
21 17
     """
22
-    help = "Check unconfirmed notifications and clean them up"
18
+    help = "Check unconfirmed alerts and clean them up"
19
+
23 20
     option_list = BaseCommand.option_list + (
24 21
         make_option('--days',
25 22
             dest='days',
26 23
             default=0,
27
-            help='cleanup notifications older then DAYS from now.'),
24
+            help='cleanup alerts older then DAYS from now.'),
28 25
         make_option('--hours',
29 26
             dest='hours',
30 27
             default=0,
31
-            help='cleanup notifications older then HOURS from now.'),
28
+            help='cleanup alerts older then HOURS from now.'),
32 29
         )
33 30
 
34 31
     def handle(self, *args, **options):
35 32
         """
36 33
         Generate a threshold date from the input options or 24 hours
37
-        if no options specified. All notifications that have the
34
+        if no options specified. All alerts that have the
38 35
         status ``UNCONFIRMED`` and have been created before the
39 36
         threshold date will be removed assuming that the emails
40 37
         are wrong or the customer changed their mind.
41 38
         """
42 39
         delta = timedelta(days=int(options['days']),
43 40
                           hours=int(options['hours']))
44
-
45 41
         if not delta:
46 42
             delta = timedelta(hours=24)
47 43
 
48
-        threshold_date = datetime.now() - delta
44
+        threshold_date = now() - delta
49 45
 
50
-        logger.info('cleaning up unconfirmed notifications older than %s',
46
+        logger.info('Deleting unconfirmed alerts older than %s',
51 47
                     threshold_date.strftime("%Y-%m-%d %H:%M"))
52 48
 
53
-        ProductNotification.objects.filter(
54
-            status=ProductNotification.UNCONFIRMED,
49
+        qs = ProductAlert.objects.filter(
50
+            status=ProductAlert.UNCONFIRMED,
55 51
             date_created__lt=threshold_date
56
-        ).delete()
52
+        )
53
+        logger.info("Found %d stale alerts to delete", qs.count())
54
+        qs.delete()

+ 25
- 0
oscar/management/commands/oscar_send_alerts.py Vedi File

@@ -0,0 +1,25 @@
1
+import logging
2
+
3
+from django.utils.translation import ugettext_lazy as _
4
+from django.core.management.base import BaseCommand
5
+
6
+from oscar.apps.customer.alerts import utils
7
+
8
+logger = logging.getLogger(__name__)
9
+
10
+
11
+class Command(BaseCommand):
12
+    """
13
+    Check stock records of products for availability and send out alerts
14
+    to customers that have registered for an alert.
15
+    """
16
+    help = _("Check check for products that are back in "
17
+             "stock and send out alerts")
18
+
19
+    def handle(self, **options):
20
+        """
21
+        Check all products with active product alerts for
22
+        availability and send out email alerts when a product is
23
+        available to buy.
24
+        """
25
+        utils.send_alerts()

+ 0
- 40
oscar/management/commands/oscar_send_notifications.py Vedi File

@@ -1,40 +0,0 @@
1
-import logging
2
-
3
-from django.db.models import get_model
4
-from django.utils.translation import ugettext_lazy as _
5
-from django.core.management.base import NoArgsCommand
6
-
7
-from oscar.apps.catalogue.notification import utils
8
-
9
-Product = get_model('catalogue', 'product')
10
-ProductNotification = get_model('notification', 'productnotification')
11
-
12
-logger = logging.getLogger(__name__)
13
-
14
-
15
-class Command(NoArgsCommand):
16
-    """
17
-    Check stock records of products for availability and send out notifications
18
-    to customers that have subscribed to a notification email. Notifications
19
-    for available products are disabled after an email has been send out.
20
-    """
21
-    help = _("Check check for notifications of products that are back in "
22
-             "stock, send out emails and deactivate the notifications")
23
-
24
-    def handle_noargs(self, **options):
25
-        """
26
-        Check all products with active product notifications for
27
-        availability and send out email notifications when a product is
28
-        available to buy.
29
-        """
30
-        logger.info('start searching for updated stock records')
31
-
32
-        products = Product.objects.filter(
33
-            productnotification__status=ProductNotification.ACTIVE
34
-        )
35
-
36
-        for product in products:
37
-            logger.info('checking product availability for %s', product)
38
-            if product.is_available_to_buy:
39
-                logger.info('sending notifications for product %s', product)
40
-                utils.send_email_notifications_for_product(product)

+ 2
- 2
oscar/templates/oscar/catalogue/browse.html Vedi File

@@ -69,7 +69,7 @@
69 69
 		<section>
70 70
 			<div class="mod">
71 71
 				{% if page_obj %}
72
-					{% include "partials/pagination.html" %}
72
+					{% include "catalogue/partials/pagination.html" %}
73 73
 				{% endif %}
74 74
 				<ol class="products four">
75 75
 					{% for product in products %}
@@ -77,7 +77,7 @@
77 77
 					{% endfor %}
78 78
 				</ol>
79 79
 				{% if page_obj %}
80
-					{% include "partials/pagination.html" %}
80
+					{% include "catalogue/partials/pagination.html" %}
81 81
 				{% endif %}
82 82
 			</div>
83 83
 		</section>

+ 3
- 4
oscar/templates/oscar/catalogue/detail.html Vedi File

@@ -60,10 +60,9 @@
60 60
 					{% endfor %}
61 61
 				</ul>
62 62
 			{% else %}
63
-			<p class="star">{% trans "No Rating" %}
64
-			
65
-			<a href="{% url catalogue:reviews-add product.slug product.id %}#addreview">{% trans "Add review" %}</a>
66
-			</p>
63
+				<p class="star">
64
+					<a href="{% url catalogue:reviews-add product.slug product.id %}#addreview">{% trans "Add review" %}</a>
65
+				</p>
67 66
 			{% endif %}
68 67
              
69 68
             <hr/>

+ 7
- 0
oscar/templates/oscar/catalogue/partials/add_to_basket_form.html Vedi File

@@ -10,4 +10,11 @@
10 10
 			<button type="submit" class="btn btn-large btn-primary" value="Add to basket">{% trans "Add to basket" %}</button>
11 11
         </div>
12 12
     </form>
13
+{% else %}
14
+	<form id="alert_form" method="post" action="{% url customer:alert-create product.id %}">
15
+        {% csrf_token %}
16
+		<p>{% trans "You can get an email alert when this product is back in stock." %}</p>
17
+        {% include "partials/form_fields.html" with form=alert_form %}
18
+        <button type="submit" class="btn btn-large btn-info">{% trans "Notify me" %}</button>
19
+    </form>
13 20
 {% endif %}

+ 1
- 1
oscar/templates/oscar/catalogue/partials/product.html Vedi File

@@ -28,7 +28,7 @@
28 28
     <h3><a href="{{ product.get_absolute_url }}">{{ product.get_title|truncatewords:7 }}</a></h3>
29 29
     
30 30
     <div class="product_price">
31
-        {% include "catalogue/partials/stock_record.html" with anon_notifications="hide" %}
31
+        {% include "catalogue/partials/stock_record.html" %}
32 32
         {% include "catalogue/partials/add_to_basket_form_compact.html" %}
33 33
    </div>
34 34
 </article>

+ 2
- 7
oscar/templates/oscar/catalogue/partials/stock_record.html Vedi File

@@ -1,19 +1,14 @@
1 1
 {% load currency_filters %}
2 2
 {% load i18n %}
3
-{% load notification_tags %}
4 3
 
5 4
 {% if product.is_group %}
6 5
     <h2 class="price_color">{% blocktrans with product.min_variant_price_incl_tax|currency as price %}From {{ price }}{% endblocktrans %}</h2>
7 6
 {% else %}
8 7
     {% if product.is_available_to_buy %} 
8
+
9 9
         <h2 class="price_color">{{ product.stockrecord.price_incl_tax|currency }}</h2>
10 10
         <p class="app-ico {{ product.stockrecord.availability_code }} availability ">{{ product.stockrecord.availability|truncatewords:2 }}</p>
11
-        {% has_product_notification user product as notification_exists %}
12
-        {% if not notification_exists %}
13
-            <p>{% include "notification/partials/notification_form.html" with form=notification_form anon_notifications=anon_notifications %}</p>
14
-        {% else %}
15
-	    <p class="alert alert-success">{% trans "You will be notified when this product is available." %}</p>
16
-        {% endif %}
11
+
17 12
     {% else %}
18 13
 		<p class="app-ico avaliability outofstock">{% trans "Not available" %}</p>
19 14
     {% endif %}

+ 13
- 0
oscar/templates/oscar/customer/alerts/emails/alert_body.txt Vedi File

@@ -0,0 +1,13 @@
1
+{% load i18n %}{% if alert.user and alert.user.first_name %}{% blocktrans with name=alert.user.first_name %}Dear {{ name }},{% endblocktrans %}{% else %}{% trans "Hello," %}{% endif %}
2
+{% blocktrans with product=alert.product path=alert.product.get_absolute_url %}
3
+We are happy to inform you that our product '{{ product }}' is back in stock:
4
+http://{{ site }}{{ path }}
5
+{% endblocktrans %}{% if hurry %}{% blocktrans with product_url=product_url %}
6
+Beware that the amount of items in stock is limited. Be quick or someone might get there first.
7
+{% endblocktrans %}{% endif %}{% blocktrans with site_name=site.name %}
8
+With this email we have disabled your alert automatically and you will not
9
+receive any further email regarding this product.
10
+
11
+Thanks for your interest,
12
+The {{ site_name }} Team
13
+{% endblocktrans %}

+ 1
- 0
oscar/templates/oscar/customer/alerts/emails/alert_subject.txt Vedi File

@@ -0,0 +1 @@
1
+{% load i18n %}{% blocktrans with title=product.get_title  %}{{ title }} is back in stock{% endblocktrans %}

+ 13
- 0
oscar/templates/oscar/customer/alerts/emails/confirmation_body.txt Vedi File

@@ -0,0 +1,13 @@
1
+{% load i18n %}{% blocktrans with product=alert.product 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
+'{{ product }}' 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 %}

+ 1
- 0
oscar/templates/oscar/customer/alerts/emails/confirmation_subject.txt Vedi File

@@ -0,0 +1 @@
1
+{% load i18n %}{% blocktrans %}Confirmation required for stock alert{% endblocktrans %}

+ 1
- 0
oscar/templates/oscar/customer/alerts/form.html Vedi File

@@ -0,0 +1 @@
1
+{% extends 'catalogue/detail.html' %}

+ 4
- 0
oscar/templates/oscar/customer/alerts/message.html Vedi File

@@ -0,0 +1,4 @@
1
+{% load i18n %}
2
+{% blocktrans with url=product.get_absolute_url title=product.get_title %}
3
+<a href="{{ url }}">{{ title }}</a> is back in stock
4
+{% endblocktrans %}

+ 40
- 4
oscar/templates/oscar/customer/profile.html Vedi File

@@ -31,7 +31,7 @@
31 31
         <li {% if active_tab == 'orders' %}class="active"{% endif %}><a href="#orders" data-toggle="tab">{% trans 'Order History' %}</a></li>
32 32
         <li {% if active_tab == 'addresses' %}class="active"{% endif %}><a href="#addresses" data-toggle="tab">{% trans 'Address Book' %}</a></li>
33 33
         <li {% if active_tab == 'emails' %}class="active"{% endif %}><a href="#emails" data-toggle="tab">{% trans 'Email History' %}</a></li>
34
-        <li {% if active_tab == 'notifications' %}class="active"{% endif %}><a href="#notifications" data-toggle="tab">{% trans "Notifications" %}</a></li>
34
+        <li {% if active_tab == 'alerts' %}class="active"{% endif %}><a href="#alerts" data-toggle="tab">{% trans "Alerts" %}</a></li>
35 35
         {% block extra_tabs_nav %}
36 36
         {% endblock %}
37 37
     </ul>
@@ -181,9 +181,45 @@
181 181
 			{% endblock %}
182 182
         </div>
183 183
 
184
-        <div class="tab-pane {% if active_tab == 'notifications' %}active{% endif %}" id="notifications">
185
-            {% block tab_notifications %}
186
-                {% include "notification/partials/notification_pane.html" with edit_enabled="yes" %}
184
+        <div class="tab-pane {% if active_tab == 'alerts' %}active{% endif %}" id="alerts">
185
+            {% block tab_alerts %}
186
+            <div class="sub-header">
187
+			    <h3>{% trans "Alerts" %}</h3>
188
+			</div>
189
+
190
+			{% if not alerts %}
191
+			<p>{% trans "You do not have any active alerts for out-of-stock products." %}</p>
192
+			{% else %}
193
+			    <form action="." method="post" id="alerts_form">
194
+			    {% csrf_token %}
195
+			        <table class="table table-stiped table-bordered">
196
+			            <tr>
197
+			                <th>{% trans "Product" %}</th>
198
+			                <th>{% trans "Status" %}</th>
199
+			                <th>{% trans "Date created" %}</th>
200
+			                <th></th>
201
+			            </tr>
202
+
203
+			            {% for alert in alerts %}
204
+			            <tr>
205
+			                <td>
206
+			                    {% with product=alert.product %}
207
+			                    <a href="{{ product.get_absolute_url }}">{{ product.get_title }}</a>
208
+			                    {% endwith %}
209
+			                </td>
210
+			                <td>{{ alert.status }}</td>
211
+			                <td>{{ alert.date_created }}</td>
212
+			                <td>
213
+			                    {% if alert.can_be_cancelled %}
214
+			                        <button class="btn btn-danger" type='submit' name='cancel_alert' value='{{ alert.id }}'>{% trans "Cancel" %}</button>
215
+			                    {% endif %}
216
+			                </td>
217
+			            </tr>
218
+			            {% endfor %}
219
+			        </table>
220
+			    </form>
221
+			{% endif %}
222
+
187 223
             {% endblock %}
188 224
         </div>
189 225
 

+ 0
- 61
oscar/templates/oscar/dashboard/notification/delete.html Vedi File

@@ -1,61 +0,0 @@
1
-{% extends 'dashboard/layout.html' %}
2
-{% load i18n %}
3
-
4
-{% block body_class %}users{% endblock %}
5
-
6
-{% block title %}
7
-{% blocktrans with id=notification.id %}Notification #{{ id }}{% endblocktrans %} | {{ block.super }}
8
-{% endblock %}
9
-
10
-{% block breadcrumbs %}
11
-<ul class="breadcrumb">
12
-    <li>
13
-    <a href="{% url dashboard:index %}">{% trans "Dashboard" %}</a>
14
-        <span class="divider">/</span>
15
-    </li>
16
-    <li>
17
-    <a href="{% url dashboard:user-notification-list %}">{% trans "Notifications" %}</a>
18
-        <span class="divider">/</span>
19
-    </li>
20
-    <li class="active"><a href=".">{% blocktrans with id=notification.id %}Notification #{{ id }}{% endblocktrans %}</a></li>
21
-</ul>
22
-{% endblock %}
23
-
24
-{% block header %}
25
-    <div class="page-header">
26
-        <h1>{% blocktrans with id=notification.id %}Delete Notification #{{ id }}?{% endblocktrans %}</h1>
27
-    </div>
28
-{% endblock header %}
29
-
30
-{% block dashboard_content %}
31
-    {% trans "Are you sure that you want to delete this notification:" %}
32
-    <form action="." method="post">
33
-        {% csrf_token %}
34
-        <table class="table table-bordered">
35
-            <tr>
36
-                <th>{% trans "Product title" %}</th>
37
-                <td>{{ notification.product.get_title }}</td>
38
-            </tr>
39
-            <tr>
40
-                <th>{% trans "User" %}</th>
41
-                <td>{% include "notification/partials/user_link.html" %}</td>
42
-            </tr>
43
-            <tr>
44
-                <th>{% trans "Email" %}</th>
45
-                <td>{{ notification.get_notification_email }}</td>
46
-            </tr>
47
-            <tr>
48
-                <th>{% trans "Date created" %}</th>
49
-                <td>{{ notification.date_created|date }}</td>
50
-            </tr>
51
-            <tr>
52
-                <th>{% trans "Date notified" %}</th>
53
-                <td>{% include "notification/partials/notified_date.html" %}</td>
54
-            </tr>
55
-        </table>
56
-        <div class="form-actions">
57
-            <button type='submit' class="btn btn-danger">{% trans "Delete" %}</button> or
58
-            <a href="{% url dashboard:user-notification-list %}">{% trans "cancel" %}</a>
59
-        </div>
60
-    </form>
61
-{% endblock dashboard_content %}

+ 0
- 98
oscar/templates/oscar/dashboard/notification/list.html Vedi File

@@ -1,98 +0,0 @@
1
-{% extends 'dashboard/layout.html' %}
2
-{% load i18n %}
3
-{% load currency_filters %}
4
-{% load notification_tags %}
5
-
6
-{% block body_class %}users{% endblock %}
7
-
8
-{% block title %}
9
-{% trans "Notification management" %} | {{ block.super }}
10
-{% endblock %}
11
-
12
-{% block breadcrumbs %}
13
-<ul class="breadcrumb">
14
-    <li>
15
-        <a href="{% url dashboard:index %}">{% trans "Dashboard" %}</a>
16
-        <span class="divider">/</span>
17
-    </li>
18
-    <li class="active">
19
-        <a href="{% url dashboard:user-notification-list %}">{% trans "Notifications" %}</a>
20
-    </li>
21
-</ul>
22
-{% endblock %}
23
-
24
-{% block header %}
25
-<div class="page-header action">
26
-    <h1>{% trans "Notification management" %}</h1>
27
-</div>
28
-{% endblock header %}
29
-
30
-{% block dashboard_content %}
31
-
32
-<div class="well well-info">
33
-    <h3 class="app-ico ico_magnify icon">{% trans "Search product notifications" %}</h3>
34
-    <form action="." method="get" class="form-inline">
35
-		{% include "partials/form_fields_inline.html" with form=form %}
36
-    <button type="submit" class="btn btn-primary">{% trans "Search" %}</button>
37
-    </form>
38
-</div>
39
-
40
-<div class="sub-header">
41
-    <h2>{{ queryset_description }}</h2>
42
-</div>
43
-
44
-{% if notification_list.all|length > 0 %}
45
-    <form action="." method="post">
46
-        {% csrf_token %}
47
-    <table class="table table-striped table-bordered">
48
-        <tr>
49
-            <th></th>
50
-            <th>{% trans "Product" %}</th>
51
-            <th>{% trans "User" %}</th>
52
-            <th>{% trans "Status" %}</th>
53
-            <th>{% trans "Date created" %}</th>
54
-            <th>{% trans "Date notified" %}</th>
55
-            <th></th>
56
-        </tr>
57
-        {% for notification in notification_list %}
58
-        <tr>
59
-            <td>
60
-                <input type="checkbox" name="selected_notification" class="selected_notification" value="{{ notification.id }}" />
61
-            </td>
62
-            <td>
63
-				<a href="{{ notification.product.get_absolute_url }}">{{ notification.product }}</a>
64
-            </td>
65
-            <td>
66
-                {% include "notification/partials/user_link.html" %}
67
-            </td>
68
-            <td>
69
-                {% include "notification/partials/status.html" %}
70
-            </td>
71
-            <td>{{ notification.date_created|date }}</td>
72
-            <td>{% include "notification/partials/notified_date.html" %}</td>
73
-            <td>
74
-                <a href="{% url dashboard:user-notification-update notification.id %}" class="btn btn-info">{% trans "Edit" %}</a>
75
-                <a href="{% url dashboard:user-notification-delete notification.id %}" class="btn btn-danger">{% trans "Delete" %}</a>
76
-            </td>
77
-        </tr>
78
-        {% endfor %}
79
-    </table>
80
-
81
-    <div class="well well-danger">
82
-        {% trans "Update status of selected notifications to:" %}
83
-        {{ notification_form.status }}
84
-	<input type="hidden" name="action" value="update_selected_notification_status" />
85
-        <button type="submit" class="btn btn-primary">{% trans "Update" %}</button>
86
-    </div>
87
-
88
-    {% if page_obj %}
89
-        {% include "catalogue/partials/pagination.html" %}
90
-    {% endif %}
91
-
92
-    </form>
93
-
94
-{% else %}
95
-<p>{% trans "No notifications found." %}</p>
96
-{% endif %}
97
-
98
-{% endblock dashboard_content %}

+ 0
- 40
oscar/templates/oscar/dashboard/notification/update.html Vedi File

@@ -1,40 +0,0 @@
1
-{% extends 'dashboard/layout.html' %}
2
-{% load i18n %}
3
-
4
-{% block body_class %}users{% endblock %}
5
-
6
-{% block title %}
7
-{% blocktrans with id=notification.id %}Update notification #{{ id }}{% endblocktrans %} | {{ block.super }}
8
-{% endblock %}
9
-
10
-{% block breadcrumbs %}
11
-<ul class="breadcrumb">
12
-    <li>
13
-        <a href="{% url dashboard:index %}">{% trans "Dashboard" %}</a>
14
-        <span class="divider">/</span>
15
-    </li>
16
-    <li>
17
-        <a href="{% url dashboard:user-notification-list %}">{% trans "Notifications" %}</a>
18
-        <span class="divider">/</span>
19
-    </li>
20
-    <li class="active"><a href=".">{% blocktrans with id=notification.id %}Notification #{{ id }}{% endblocktrans %}</a></li>
21
-</ul>
22
-{% endblock %}
23
-
24
-{% block header %}
25
-    <div class="page-header">
26
-        <h1>{% blocktrans with id=notification.id %}Notification #{{ id }}{% endblocktrans %}</h1>
27
-    </div>
28
-{% endblock header %}
29
-
30
-{% block dashboard_content %}
31
-<form action="." method="post">
32
-    {% csrf_token %}
33
-
34
-	{% include 'partials/form_fields.html' with form=form %}
35
-    <div class="form-actions">
36
-        <button type='submit' class="btn btn-large btn-primary">{% trans "Save" %}</button> or
37
-        <a href="{% url dashboard:user-notification-list %}">{% trans "cancel" %}</a>
38
-    </div>
39
-</form>
40
-{% endblock dashboard_content %}

+ 40
- 0
oscar/templates/oscar/dashboard/users/alerts/delete.html Vedi File

@@ -0,0 +1,40 @@
1
+{% extends 'dashboard/layout.html' %}
2
+{% load i18n %}
3
+
4
+{% block body_class %}users{% endblock %}
5
+
6
+{% block title %}
7
+{% blocktrans with id=alert.id %}Alert #{{ id }}{% endblocktrans %} | {{ block.super }}
8
+{% endblock %}
9
+
10
+{% block breadcrumbs %}
11
+<ul class="breadcrumb">
12
+    <li>
13
+    <a href="{% url dashboard:index %}">{% trans "Dashboard" %}</a>
14
+        <span class="divider">/</span>
15
+    </li>
16
+    <li>
17
+    <a href="{% url dashboard:user-alert-list %}">{% trans "Product alerts" %}</a>
18
+        <span class="divider">/</span>
19
+    </li>
20
+    <li class="active"><a href=".">{% blocktrans with id=alert.id %}Alert #{{ id }}{% endblocktrans %}</a></li>
21
+</ul>
22
+{% endblock %}
23
+
24
+{% block header %}
25
+    <div class="page-header">
26
+        <h1>{% blocktrans with id=alert.id %}Delete alert #{{ id }}?{% endblocktrans %}</h1>
27
+    </div>
28
+{% endblock header %}
29
+
30
+{% block dashboard_content %}
31
+    {% include 'dashboard/users/alerts/partials/alert.html' %}
32
+    <form action="." method="post">
33
+        {% csrf_token %}
34
+        <div class="form-actions">
35
+		<p>{% trans "Are you sure that you want to delete this alert?" %}</p>
36
+            <button type='submit' class="btn btn-large btn-danger">{% trans "Delete" %}</button> or
37
+            <a href="{% url dashboard:user-alert-list %}">{% trans "cancel" %}</a>
38
+        </div>
39
+    </form>
40
+{% endblock dashboard_content %}

+ 89
- 0
oscar/templates/oscar/dashboard/users/alerts/list.html Vedi File

@@ -0,0 +1,89 @@
1
+{% extends 'dashboard/layout.html' %}
2
+{% load i18n %}
3
+{% load currency_filters %}
4
+
5
+{% block body_class %}users{% endblock %}
6
+
7
+{% block title %}
8
+{% trans "Product alerts" %} | {{ block.super }}
9
+{% endblock %}
10
+
11
+{% block breadcrumbs %}
12
+<ul class="breadcrumb">
13
+    <li>
14
+        <a href="{% url dashboard:index %}">{% trans "Dashboard" %}</a>
15
+        <span class="divider">/</span>
16
+    </li>
17
+    <li class="active">
18
+        <a href="{% url dashboard:user-alert-list %}">{% trans "Product alerts" %}</a>
19
+    </li>
20
+</ul>
21
+{% endblock %}
22
+
23
+{% block header %}
24
+<div class="page-header action">
25
+    <h1>{% trans "Product alerts" %}</h1>
26
+</div>
27
+{% endblock header %}
28
+
29
+{% block dashboard_content %}
30
+
31
+<div class="well well-info">
32
+    <h3 class="app-ico ico_magnify icon">{% trans "Search product alerts" %}</h3>
33
+    <form action="." method="get" class="form-inline">
34
+		{% include "partials/form_fields_inline.html" with form=form %}
35
+	<button type="submit" class="btn btn-primary">{% trans "Search" %}</button>
36
+	{% trans "or" %}
37
+		<a href="{% url dashboard:user-alert-list %}" class="btn">{% trans "reset" %}</a>
38
+    </form>
39
+</div>
40
+
41
+<div class="sub-header">
42
+    <h2>{{ queryset_description }}</h2>
43
+</div>
44
+
45
+{% if alerts %}
46
+    <table class="table table-striped table-bordered">
47
+        <tr>
48
+            <th></th>
49
+            <th>{% trans "Product" %}</th>
50
+            <th>{% trans "User" %}</th>
51
+            <th>{% trans "Status" %}</th>
52
+            <th>{% trans "Date created" %}</th>
53
+            <th>{% trans "Date alert sent" %}</th>
54
+            <th></th>
55
+        </tr>
56
+        {% for alert in alerts %}
57
+        <tr>
58
+            <td>
59
+				<a href="{{ alert.product.get_absolute_url }}">{{ alert.product }}</a>
60
+            </td>
61
+            <td>
62
+                {% if alert.user %}
63
+				<a href="{% url dashboard:user-detail alert.user.id %}">{{ alert.user.email }}</a>
64
+				{% else %}
65
+				{{ alert.email }} {% trans "(Anonymous)" %}
66
+				{% endif %}
67
+            </td>
68
+            <td>
69
+                {{ alert.status }}
70
+            </td>
71
+            <td>{{ alert.date_created }}</td>
72
+            <td>{{ alert.date_closed|default:"-" }}</td>
73
+            <td>
74
+                <a href="{% url dashboard:user-alert-update alert.id %}" class="btn btn-info">{% trans "Edit" %}</a>
75
+                <a href="{% url dashboard:user-alert-delete alert.id %}" class="btn btn-danger">{% trans "Delete" %}</a>
76
+            </td>
77
+        </tr>
78
+        {% endfor %}
79
+    </table>
80
+
81
+    {% if page_obj %}
82
+        {% include "catalogue/partials/pagination.html" %}
83
+    {% endif %}
84
+
85
+{% else %}
86
+	<p>{% trans "No alerts found." %}</p>
87
+{% endif %}
88
+
89
+{% endblock dashboard_content %}

+ 44
- 0
oscar/templates/oscar/dashboard/users/alerts/partials/alert.html Vedi File

@@ -0,0 +1,44 @@
1
+{% load i18n %}
2
+<table class="table">
3
+	<tbody>
4
+		<tr>
5
+			<th>{% trans "Product" %}</th>
6
+			<td><a href="{{ alert.product.get_absolute_url }}">{{ alert.product }}</a></td>
7
+		</tr>
8
+		<tr>
9
+			<th>{% trans "User" %}</th>
10
+			<td>
11
+				{% if alert.user %}
12
+					{{ alert.user }}
13
+				{% else %}
14
+					{{ alert.email }}
15
+				{% endif %}
16
+			</td>
17
+		</tr>
18
+		<tr>
19
+			<th>{% trans "Status" %}</th>
20
+			<td>{{ alert.status }}</td>
21
+		</tr>
22
+		<tr>
23
+			<th>{% trans "Date created" %}</th>
24
+			<td>{{ alert.date_created }}</td>
25
+		</tr>
26
+		{% if not alert.user %}
27
+		<tr>
28
+			<th>{% trans "Date confirmed" %}</th>
29
+			<td>{{ alert.date_confirmed|default:"-" }}</td>
30
+		</tr>
31
+		{% endif %}
32
+		{% if alert.is_cancelled %}
33
+		<tr>
34
+			<th>{% trans "Date cancelled" %}</th>
35
+			<td>{{ alert.date_cancelled }}</td>
36
+		</tr>
37
+		{% else %}
38
+		<tr>
39
+			<th>{% trans "Date alert sent" %}</th>
40
+			<td>{{ alert.date_closed|default:"-" }}</td>
41
+		</tr>
42
+		{% endif %}
43
+	</tbody>
44
+</table>

+ 43
- 0
oscar/templates/oscar/dashboard/users/alerts/update.html Vedi File

@@ -0,0 +1,43 @@
1
+{% extends 'dashboard/layout.html' %}
2
+{% load i18n %}
3
+
4
+{% block body_class %}users{% endblock %}
5
+
6
+{% block title %}
7
+{% blocktrans with id=alert.id %}Update alert #{{ id }}{% endblocktrans %} | {{ block.super }}
8
+{% endblock %}
9
+
10
+{% block breadcrumbs %}
11
+<ul class="breadcrumb">
12
+    <li>
13
+        <a href="{% url dashboard:index %}">{% trans "Dashboard" %}</a>
14
+        <span class="divider">/</span>
15
+    </li>
16
+    <li>
17
+        <a href="{% url dashboard:user-alert-list %}">{% trans "Product alerts" %}</a>
18
+        <span class="divider">/</span>
19
+    </li>
20
+    <li class="active"><a href=".">{% blocktrans with id=alert.id %}Alert #{{ id }}{% endblocktrans %}</a></li>
21
+</ul>
22
+{% endblock %}
23
+
24
+{% block header %}
25
+    <div class="page-header">
26
+        <h1>{% blocktrans with id=alert.id %}Product alert #{{ id }}{% endblocktrans %}</h1>
27
+    </div>
28
+{% endblock header %}
29
+
30
+{% block dashboard_content %}
31
+{% include 'dashboard/users/alerts/partials/alert.html' %}
32
+
33
+<h2>{% trans "Change status" %}</h2>
34
+<form action="." method="post">
35
+    {% csrf_token %}
36
+
37
+	{% include 'partials/form_fields.html' %}
38
+    <div class="form-actions">
39
+		<button type='submit' class="btn btn-large btn-primary">{% trans "Save" %}</button> {% trans "or" %}
40
+        <a href="{% url dashboard:user-alert-list %}">{% trans "cancel" %}</a>
41
+    </div>
42
+</form>
43
+{% endblock dashboard_content %}

+ 0
- 32
oscar/templates/oscar/notification/email.html Vedi File

@@ -1,32 +0,0 @@
1
-{% load i18n %}
2
-
3
-<p>
4
-{% blocktrans with site_domain=site.domain product=product site_name=site.name %}
5
-You have just signed up for a notification to {{ product }} on our
6
-website <a href="http://{{ site_domain }}">{{ site_name }}</a>. If this was you,
7
-please feel confirm your email address by clicking the following link or
8
-copying it into the address bar of your favourite browser.
9
-{% endblocktrans %}
10
-</p>
11
-<p>
12
-<a href="http://{{ site.domain }}{{ notification.get_confirm_url }}">http://{{ site.domain }}{{ notification.get_confirm_url }}</a>
13
-</p>
14
-<p>
15
-{% url catalogue:detail product.slug product.id as product_url %}
16
-
17
-{% blocktrans with email=notification.email product_title=product.title product_url=product_url %}
18
-We will send you an email to {{ email }} as soon as
19
-<a href="http://{{ site.domain }}{{ product_url }}">{{ product_title }}</a> is back in stock.
20
-{% endblocktrans %}
21
-</p>
22
-
23
-<p>
24
-{% blocktrans %}
25
-If you want to disable your notification at any point in time, use the
26
-following link to unsubscribe:
27
-{% endblocktrans %}
28
-</p>
29
-
30
-<p><a href="http://{{ site.domain }}{{ notification.get_unsubscribe_url }}">http://{{ site.domain }}{{ notification.get_unsubscribe_url }}</a></p>
31
-
32
-<strong>{% trans "Thank you very much for your interest." %}</strong>

+ 0
- 59
oscar/templates/oscar/notification/notification.html Vedi File

@@ -1,59 +0,0 @@
1
-{% extends "catalogue/detail.html" %}
2
-{% load currency_filters %}
3
-{% load history_tags %}
4
-{% load i18n %}
5
-
6
-{% block content %}
7
-<article class="product_page"><!-- Start of product page -->
8
-    <div class="row-fluid">
9
-        <div class="span6">
10
-            {% include "catalogue/partials/gallery.html" %}
11
-        </div><!-- /span6 -->
12
-
13
-        <div class="span6">
14
-            <h1>
15
-            {% blocktrans with product_title=product.get_title %}
16
-            Notification for {{ product_title }}
17
-            {% endblocktrans %}
18
-            </h1>
19
-
20
-            {% include "notification/partials/notification_form.html" with form=form %}
21
-
22
-            {% if reviews %}
23
-                    <ul class="review_count">
24
-                            {% for review in reviews %}
25
-                            <li class="{{ review.score }}">{{ review.score }}</li>
26
-            {% endfor %}
27
-                    </ul>
28
-            {% else %}
29
-            <p class="star">{% trans "No Rating" %}
30
-
31
-            <a href="{% url catalogue:reviews-add product.slug product.id %}#addreview">{% trans "Add review" %}</a>
32
-            </p>
33
-            {% endif %}
34
-
35
-            <p><strong>{% trans "Product Code:" %}</strong> <code>{{ product.upc }}</code></p>
36
-
37
-            <hr/>
38
-
39
-			{% if product.description %}
40
-            <ul>
41
-				<li><a href="#product_description">{% trans "Read the full description" %}</a></li>
42
-            </ul>
43
-			{% endif %}
44
-
45
-            {% include "catalogue/partials/add_to_basket_form.html" %}
46
-        </div><!-- /span6 -->
47
-    </div><!-- /row-fluid -->
48
-
49
-	{% block product_description %}
50
-	{% if product.description %}
51
-    <div id="product_description" class="sub-header">
52
-		<h2>{% trans "Product Description" %}</h2>
53
-    </div>
54
-    <p>{{ product.description }}</p>
55
-	{% endif %}
56
-	{% endblock %}
57
-
58
-</article><!-- End of product page -->
59
-{% endblock content %}

+ 0
- 30
oscar/templates/oscar/notification/notification_email.html Vedi File

@@ -1,30 +0,0 @@
1
-{% load i18n %}
2
-
3
-{% if user and user.get_full_name %}
4
-	{% blocktrans with full_name=user.get_full_name %}
5
-	Dear {{ full_name }},
6
-	{% endblocktrans %}
7
-{% else %}
8
-	{% trans "Dear Customer," %}
9
-{% endif %}
10
-
11
-{% url catalogue:detail product.slug product.id as product_url %}
12
-{% blocktrans with product_url=product_url product_title=product.title %}
13
-we are happy to inform you that our product <strong><a href="{{ product_url }}">
14
-{{ product_title }}</a></strong> is back in stock.
15
-{% endblocktrans %}
16
-
17
-{% if hurry %}
18
-{% blocktrans with product_url=product_url %}
19
-The amount of items in stock is limited. Head over to <a href="{{ product_url }}">
20
-order the product</a> now, before someone else get's there first.
21
-{% endblocktrans %}
22
-{% endif %}
23
-
24
-{% blocktrans with site_name=site.name %}
25
-With this email we have disabled your notification automatically and you will not
26
-receive any further email regarding this product.
27
-
28
-Thanks for your interest,
29
-Your {{ site_name }} Team
30
-{% endblocktrans %}

+ 0
- 12
oscar/templates/oscar/notification/partials/notification_form.html Vedi File

@@ -1,12 +0,0 @@
1
-{% load i18n %}
2
-
3
-{% if product.stockrecord.num_in_stock == 0 %}
4
-    {% if user.is_authenticated or anon_notifications != "hide" %}
5
-    <form method="post" action="{% url catalogue:notification-create product.slug product.id %}">
6
-        {% csrf_token %}
7
-		<p>{% trans "You can get an email notification when this product is in stock." %}</p>
8
-        {% include "partials/form_fields.html" with form=form %}
9
-        <button type="submit" class="btn btn-info">{% trans "Notify me" %}</button>
10
-    </form>
11
-    {% endif %}
12
-{% endif %}

+ 0
- 54
oscar/templates/oscar/notification/partials/notification_pane.html Vedi File

@@ -1,54 +0,0 @@
1
-{% load i18n %}
2
-<div class="sub-header">
3
-    <h3>{% trans "Notifications" %}</h3>
4
-</div>
5
-{% if not notification_list %}
6
-<p>{% trans "You do not have any notifications for out-of-stock products." %}</p>
7
-{% else %}
8
-    <form action="." method="POST">
9
-    {% csrf_token %}
10
-        <table class="table table-stiped table-bordered">
11
-            <tr>
12
-                <th>{% trans "Product Title" %}</th>
13
-                <th>{% trans "Product Availability" %}</th>
14
-                <th>{% trans "Status" %}</th>
15
-                <th>{% trans "Date Subscribed" %}</th>
16
-                {% if edit_enabled %}
17
-                <th></th>
18
-                {% endif %}
19
-            </tr>
20
-
21
-            {% for notification in notification_list %}
22
-            <tr>
23
-                <td>
24
-                    {% with product=notification.product %}
25
-                    <a href="{% url catalogue:detail product.slug product.id %}">
26
-                        {{ product.title }}
27
-                    </a>
28
-                    {% endwith %}
29
-                </td>
30
-                <td>
31
-                    {% if notification.product.stock_record.num_in_stock > 0 %}
32
-                        {{ notification.product.stock_record.num_in_stock }}
33
-                    {% else %}
34
-                        {% trans "Not in Stock" %}
35
-                    {% endif %}
36
-                </td>
37
-                <td>
38
-                    {% include "notification/partials/status.html" %}
39
-                </td>
40
-                <td>{{ notification.date_created|date }}</td>
41
-                {% if edit_enabled == 'yes' %}
42
-                <td>
43
-                    {% if not notification.is_active %}
44
-                        <button class="btn btn-success" type='submit' name='activate' value='{{ notification.id }}'>{% trans "Activate" %}</button>
45
-                    {% else %}
46
-                        <button class="btn btn-danger" type='submit' name='deactivate' value='{{ notification.id }}'>{% trans "Deactivate" %}</button>
47
-                    {% endif %}
48
-                </td>
49
-                {% endif %}
50
-            </tr>
51
-            {% endfor %}
52
-        </table>
53
-    </form>
54
-{% endif %}

+ 0
- 6
oscar/templates/oscar/notification/partials/notified_date.html Vedi File

@@ -1,6 +0,0 @@
1
-{% load i18n %}
2
-{% if notification.date_notified %}
3
-    {{ notification.date_notified|date }}
4
-{% else %}
5
-    {% trans "Not yet notified" %}
6
-{% endif %}

+ 0
- 7
oscar/templates/oscar/notification/partials/status.html Vedi File

@@ -1,7 +0,0 @@
1
-{% if notification.is_active %}
2
-<span class="label label-success">
3
-{% else %}
4
-<span class="label label-important">
5
-{% endif %}
6
-    {{ notification.get_status_display }}
7
-</span>

+ 0
- 7
oscar/templates/oscar/notification/partials/user_link.html Vedi File

@@ -1,7 +0,0 @@
1
-{% load i18n %}
2
-
3
-{% if notification.user %}
4
-    <a href="{% url dashboard:user-detail notification.user.id %}">{{ notification.user.email }}</a>
5
-{% else %}
6
-    {% blocktrans with email=notification.get_notification_email %}{{ email }} (anonymous){% endblocktrans %}
7
-{% endif %}

+ 0
- 21
oscar/templatetags/notification_tags.py Vedi File

@@ -1,21 +0,0 @@
1
-from django import template
2
-from django.db.models import get_model
3
-
4
-ProductNotification = get_model('notification', 'productnotification')
5
-
6
-register = template.Library()
7
-
8
-
9
-@register.assignment_tag
10
-def has_product_notification(user, product):
11
-    """
12
-    Check if the user has already signed up to receive a notification
13
-    for this product. Anonymous users are ignored. If a registered
14
-    user has signed up for a notification the tag returns ``True``.
15
-    It returns ``False`` in all other cases.
16
-    """
17
-    if not user.is_authenticated():
18
-        return False
19
-
20
-    return ProductNotification.objects.filter(user=user,
21
-                                              product=product).count() > 0

+ 0
- 19
sites/sandbox/recreate_db.sh Vedi File

@@ -1,19 +0,0 @@
1
-#!/bin/bash
2
-
3
-DATABASE=db.sqlite
4
-
5
-rm $DATABASE
6
-./manage.py syncdb --noinput
7
-./manage.py migrate
8
-
9
-echo "Loading fixtures"
10
-./manage.py loaddata countries.json ../_fixtures/pages.json
11
-
12
-echo "Importing products"
13
-./manage.py oscar_import_catalogue ../_fixtures/books-catalogue.csv
14
-
15
-echo "Importing Images"
16
-./manage.py oscar_import_catalogue_images ../_fixtures/books-images.tar.gz
17
-
18
-echo "Creating superuser"
19
-./manage.py createsuperuser --username=admin --email=admin@test.com

+ 37
- 10
tests/functional/catalogue/catalogue_tests.py Vedi File

@@ -1,21 +1,48 @@
1 1
 import httplib
2 2
 
3
-from django.test import TestCase
4
-from django.test.client import Client
5 3
 from django.core.urlresolvers import reverse
6 4
 
5
+from oscar.test import WebTestCase
7 6
 from oscar.test.helpers import create_product
7
+from oscar.apps.catalogue.views import ProductListView
8 8
 
9 9
 
10
-class TestProductDetailView(TestCase):
11
-
12
-    def setUp(self):
13
-        self.client = Client()
10
+class TestProductDetailView(WebTestCase):
14 11
 
15 12
     def test_enforces_canonical_url(self):
16 13
         p = create_product()
17
-        args = {'product_slug': 'wrong-slug',
18
-                'pk': p.id}
19
-        wrong_url = reverse('catalogue:detail', kwargs=args)
20
-        response = self.client.get(wrong_url)
14
+        kwargs = {'product_slug': 'wrong-slug',
15
+                  'pk': p.id}
16
+        wrong_url = reverse('catalogue:detail', kwargs=kwargs)
17
+
18
+        response = self.app.get(wrong_url)
21 19
         self.assertEquals(httplib.MOVED_PERMANENTLY, response.status_code)
20
+
21
+
22
+class TestProductListView(WebTestCase):
23
+
24
+    def test_shows_add_to_basket_button_for_available_product(self):
25
+        product = create_product()
26
+
27
+        page = self.app.get(reverse('catalogue:index'))
28
+
29
+        self.assertContains(page, product.title)
30
+        self.assertContains(page, "Add to basket")
31
+
32
+    def test_shows_not_available_for_out_of_stock_product(self):
33
+        product = create_product(num_in_stock=0)
34
+
35
+        page = self.app.get(reverse('catalogue:index'))
36
+
37
+        self.assertContains(page, product.title)
38
+        self.assertContains(page, "Not available")
39
+
40
+    def test_shows_pagination_navigation_for_multiple_pages(self):
41
+        per_page = ProductListView.paginate_by
42
+        title = "Product #%d"
43
+        for idx in range(0, int(1.5 * per_page)):
44
+            create_product(title=title % idx)
45
+
46
+        page = self.app.get(reverse('catalogue:index'))
47
+
48
+        self.assertContains(page, "Page 1 of 2")

+ 0
- 122
tests/functional/catalogue/notification_command_tests.py Vedi File

@@ -1,122 +0,0 @@
1
-from datetime import datetime, timedelta
2
-
3
-from django.core import mail
4
-from django.test import TestCase
5
-from django.core.management import call_command
6
-
7
-from oscar.apps.catalogue.models import Product
8
-from oscar.apps.partner.models import StockRecord, Partner
9
-from oscar.apps.catalogue.notification.models import ProductNotification
10
-
11
-from django_dynamic_fixture import get as G
12
-
13
-
14
-class TestCleanNotificationsCommand(TestCase):
15
-
16
-    def setUp(self):
17
-        self.date_now = datetime.now()
18
-        status_date_mapping = [
19
-            (ProductNotification.UNCONFIRMED,
20
-             self.date_now - timedelta(minutes=45)),
21
-            (ProductNotification.ACTIVE,
22
-             self.date_now),
23
-            (ProductNotification.UNCONFIRMED,
24
-             self.date_now - timedelta(days=1, hours=3)),
25
-            (ProductNotification.UNCONFIRMED,
26
-             self.date_now - timedelta(hours=7)),
27
-            (ProductNotification.UNCONFIRMED,
28
-             self.date_now - timedelta(days=2, hours=1)),
29
-            (ProductNotification.INACTIVE,
30
-             self.date_now - timedelta(days=2)),
31
-            (ProductNotification.ACTIVE,
32
-             self.date_now - timedelta(days=2)),
33
-        ]
34
-
35
-        for status, date in status_date_mapping:
36
-            notification = G(ProductNotification, status=status)
37
-            notification.date_created = date
38
-            notification.save()
39
-
40
-    def test_cleans_up_unconfirmed_notifications_with_default_settings(self):
41
-        # Test removing all notifications that have status UNCONFIRMED and
42
-        # are older then 24 hours which is the default
43
-        self.assertEquals(ProductNotification.objects.count(), 7)
44
-        call_command('oscar_cleanup_notifications', *[], **{})
45
-
46
-        self.assertEquals(ProductNotification.objects.count(), 5)
47
-
48
-    def test_cleans_up_unconfirmed_notifications_older_than_two_days(self):
49
-        # Test removing all notifications that have status ``UNCONFIRMED``
50
-        # and remove the ones older then 2 days.
51
-        self.assertEquals(ProductNotification.objects.count(), 7)
52
-        call_command('oscar_cleanup_notifications', *[], **{'days': '2'})
53
-
54
-        self.assertEquals(ProductNotification.objects.count(), 6)
55
-
56
-    def test_cleans_up_unconfirmed_notifications_older_than_6_hours(self):
57
-        # Test removing all notifications that have status ``UNCONFIRMED``
58
-        # and remove the ones older then 6 hours.
59
-        self.assertEquals(ProductNotification.objects.count(), 7)
60
-        call_command('oscar_cleanup_notifications', *[], **{'hours': '6'})
61
-
62
-        self.assertEquals(ProductNotification.objects.count(), 4)
63
-
64
-    def test_cleans_up_unconfirmed_notifications_older_than_1_day_2_hours(self):
65
-        # Test removing all notifications that have status ``UNCONFIRMED``
66
-        # and remove the ones older then 6 hours.
67
-        self.assertEquals(ProductNotification.objects.count(), 7)
68
-        call_command('oscar_cleanup_notifications',
69
-                     *[], **{'days': 1, 'hours': '2'})
70
-
71
-        self.assertEquals(ProductNotification.objects.count(), 5)
72
-
73
-
74
-class TestSendNotificationsCommand(TestCase):
75
-
76
-    def setUp(self):
77
-        mail.outbox = []
78
-        partner = Partner.objects.create(name="Partner")
79
-
80
-        self.product1 = G(Product)
81
-        G(StockRecord, product=self.product1, partner=partner, num_in_stock=50)
82
-
83
-        self.product2 = G(Product)
84
-        G(StockRecord, product=self.product2, partner=partner, num_in_stock=0)
85
-
86
-        self.product3 = G(Product)
87
-        G(StockRecord, product=self.product3, partner=partner, num_in_stock=1)
88
-
89
-    def test_sends_notifications_for_products_back_in_stock(self):
90
-        active_notif1 = G(ProductNotification, status=ProductNotification.ACTIVE,
91
-                         product=self.product1, date_notified=None)
92
-        active_notif2 = G(ProductNotification, status=ProductNotification.ACTIVE,
93
-                         product=self.product1, date_notified=None)
94
-        active_notif3 = G(ProductNotification, status=ProductNotification.ACTIVE,
95
-                         product=self.product2, date_notified=None)
96
-        active_notif4 = G(ProductNotification, status=ProductNotification.ACTIVE,
97
-                         product=self.product3, date_notified=None)
98
-
99
-        inactive_notif1 = G(ProductNotification, status=ProductNotification.INACTIVE,
100
-                         product=self.product1, date_notified=None)
101
-        inactive_notif2 = G(ProductNotification, status=ProductNotification.INACTIVE,
102
-                         product=self.product2, date_notified=None)
103
-
104
-        self.assertEquals(mail.outbox, [])
105
-
106
-        call_command('oscar_send_notifications')
107
-
108
-        for pk in [active_notif1.pk, active_notif2.pk, active_notif4.pk]:
109
-            notif = ProductNotification.objects.get(pk=pk)
110
-            self.assertEquals(notif.status, ProductNotification.INACTIVE)
111
-            self.assertNotEqual(notif.date_notified, None)
112
-
113
-        notif = ProductNotification.objects.get(pk=active_notif3.pk)
114
-        self.assertEquals(notif.status, ProductNotification.ACTIVE)
115
-        self.assertEquals(notif.date_notified, None)
116
-
117
-        self.assertEquals(len(mail.outbox), 3)
118
-
119
-        for pk in [inactive_notif1.pk, inactive_notif2.pk]:
120
-            notif = ProductNotification.objects.get(pk=pk)
121
-            self.assertEquals(notif.status, ProductNotification.INACTIVE)
122
-            self.assertEquals(notif.date_notified, None)

+ 0
- 308
tests/functional/catalogue/notification_tests.py Vedi File

@@ -1,308 +0,0 @@
1
-import itertools
2
-
3
-from django.core import mail
4
-from django.db.models import get_model
5
-from django.contrib.auth.models import User
6
-from django.core.urlresolvers import reverse
7
-from django_dynamic_fixture import get
8
-
9
-from webtest.app import AppError
10
-from django_webtest import WebTest
11
-from django_dynamic_fixture import get as G
12
-
13
-from oscar.apps.catalogue.models import Product, ProductClass
14
-from oscar.apps.partner.models import StockRecord
15
-from oscar.apps.catalogue.notification.models import ProductNotification
16
-
17
-Partner = get_model('partner', 'partner')
18
-
19
-
20
-class NotificationWebTest(WebTest):
21
-    product_counter = itertools.count()
22
-    username = 'testuser'
23
-    password = 'somerandompassword'
24
-    email = 'testuser@example.com'
25
-    is_anonymous = True
26
-
27
-    def setUp(self):
28
-        self.user = None
29
-
30
-        if not self.is_anonymous:
31
-            self.user = User.objects.create(username=self.username,
32
-                                            password=self.password,
33
-                                            email=self.email)
34
-
35
-    def create_product_class(self, name='books'):
36
-        self.product_class = ProductClass.objects.create(name=name)
37
-
38
-    def create_product(self):
39
-        product_id = self.product_counter.next()
40
-        product = get(Product, product_class=self.product_class,
41
-                      title='product_%s' % product_id,
42
-                      upc='00000000000%s' % product_id, parent=None)
43
-
44
-        G(StockRecord, product=product, num_in_stock=0)
45
-        return product
46
-
47
-    def get(self, *args, **kwargs):
48
-        if self.user and not 'user' in kwargs:
49
-            kwargs['user'] = self.user
50
-        return self.app.get(*args, **kwargs)
51
-
52
-    def post(self, *args, **kwargs):
53
-        if self.user and not 'user' in kwargs:
54
-            kwargs['user'] = self.user
55
-        return self.app.post(*args, **kwargs)
56
-
57
-
58
-class TestNotifyMeButtons(NotificationWebTest):
59
-
60
-    def setUp(self):
61
-        self.create_product_class()
62
-        self.product = self.create_product()
63
-
64
-    def test_are_displayed_on_unavailable_product_page(self):
65
-
66
-        url = reverse('catalogue:detail', args=(self.product.slug,
67
-                                                self.product.id))
68
-        page = self.app.get(url)
69
-        self.assertContains(page, 'notify-me', status_code=200)
70
-
71
-    def test_are_not_displayed_on_available_product_page(self):
72
-        self.product.stockrecord.num_in_stock = 20
73
-        self.product.stockrecord.save()
74
-
75
-        url = reverse('catalogue:detail', args=(self.product.slug,
76
-                                                self.product.id))
77
-        page = self.app.get(url)
78
-        self.assertNotContains(page, 'notify-me', status_code=200)
79
-
80
-        self.product.stockrecord.num_in_stock = 1
81
-        self.product.stockrecord.save()
82
-
83
-        page = self.app.get(url)
84
-        self.assertNotContains(page, 'notify-me', status_code=200)
85
-
86
-
87
-class TestAnAnonymousUserRequestingANotification(NotificationWebTest):
88
-    is_anonymous = True
89
-    email = 'anonymous@email.com'
90
-
91
-    def setUp(self):
92
-        super(TestAnAnonymousUserRequestingANotification, self).setUp()
93
-        self.create_product_class()
94
-        self.out_of_stock_product = self.create_product()
95
-
96
-    def create_notification(self):
97
-        page = self.get(reverse('catalogue:detail', args=(
98
-            self.out_of_stock_product.slug,
99
-            self.out_of_stock_product.id,
100
-        )))
101
-        notify_form = page.forms[1]
102
-        notify_form['email'] = self.email
103
-        return notify_form.submit()
104
-
105
-    def test_submitting_an_invalid_email_redirects_back_to_page(self):
106
-        page = self.get(reverse('catalogue:detail', args=(
107
-            self.out_of_stock_product.slug,
108
-            self.out_of_stock_product.id,
109
-        )))
110
-        notify_form = page.forms[1]
111
-        notify_form['email'] = u"invalid_email.com"
112
-        page = notify_form.submit()
113
-
114
-        self.assertContains(page, "Enter a valid e-mail address.")
115
-
116
-    def test_creates_an_unconfirmed_notification_and_sends_confirmation_email(self):
117
-        # Test creating a notification for an anonymous user. A notification
118
-        # is generated for the user with confirmation and unsubscribe code.
119
-        # The notification is set to UNCONFIRMED and a email is sent to the
120
-        # user. The confirmation of the notification is handled by a link
121
-        # that will activate the notification.
122
-        self.create_notification()
123
-
124
-        notifications = ProductNotification.objects.filter(email=self.email)
125
-        self.assertEquals(notifications.count(), 1)
126
-
127
-        self.assertEquals(len(mail.outbox), 1)
128
-        self.assertTrue("confirm" in mail.outbox[0].body)
129
-        self.assertTrue("unsubscribe" in mail.outbox[0].body)
130
-
131
-
132
-    def test_can_activate_an_unconfirmed_notification(self):
133
-        self.test_creates_an_unconfirmed_notification_and_sends_confirmation_email()
134
-
135
-        notification = ProductNotification.objects.get(email=self.email)
136
-        product = notification.product
137
-
138
-        self.assertEquals(notification.status, ProductNotification.UNCONFIRMED)
139
-
140
-        page = self.get(notification.get_confirm_url())
141
-        self.assertRedirects(
142
-            page,
143
-            reverse('catalogue:detail', args=(product.slug, product.id)),
144
-            status_code=301
145
-        )
146
-
147
-        notification = ProductNotification.objects.get(id=notification.id)
148
-        self.assertEquals(notification.status, ProductNotification.ACTIVE)
149
-
150
-    def test_can_unsubscribe_from_a_notification(self):
151
-        # Test that unsubscribing from a notification inactivates the
152
-        # notification. This does not delete the notification as it might be
153
-        # used for analytical purposes later on by the site owner.
154
-        self.create_notification()
155
-
156
-        notification = ProductNotification.objects.get(email=self.email)
157
-        product = notification.product
158
-
159
-        self.assertEquals(notification.status, ProductNotification.UNCONFIRMED)
160
-
161
-        page = self.get(notification.get_unsubscribe_url())
162
-        self.assertRedirects(
163
-            page,
164
-            reverse('catalogue:detail', args=(product.slug, product.id)),
165
-            status_code=301
166
-        )
167
-
168
-        notification = ProductNotification.objects.get(email=self.email)
169
-        self.assertEquals(notification.status, ProductNotification.INACTIVE)
170
-
171
-
172
-class TestARegisteredUserRequestingANotification(NotificationWebTest):
173
-    is_anonymous = False
174
-    email = 'testuser@oscar.com'
175
-
176
-    def setUp(self):
177
-        super(TestARegisteredUserRequestingANotification, self).setUp()
178
-        self.create_product_class()
179
-        self.product = self.create_product()
180
-
181
-    def test_sees_email_on_product_page(self):
182
-        product_url = reverse('catalogue:detail',
183
-                              args=(self.product.slug, self.product.id))
184
-        page = self.get(product_url)
185
-
186
-        self.assertContains(page, self.email, status_code=200)
187
-
188
-    def test_creates_a_notification_object(self):
189
-        # Test creating a notification for an authenticated user with the
190
-        # providing the account email address in the (hidden) signup form.
191
-        self.assertEquals(self.user.product_notifications.count(), 0)
192
-
193
-        page = self.get(reverse('catalogue:detail', args=(self.product.slug,
194
-                                                          self.product.id)))
195
-        notification_form = page.forms[1]
196
-        notification_form['email'] = self.email
197
-        page = notification_form.submit().follow()
198
-
199
-        self.assertContains(page, self.product.title, status_code=200)
200
-        self.assertEquals(self.user.product_notifications.count(), 1)
201
-
202
-        notification = self.user.product_notifications.all()[0]
203
-        self.assertEquals(notification.get_notification_email(),
204
-                          self.user.email)
205
-        self.assertEquals(notification.confirm_key, None)
206
-        self.assertEquals(notification.unsubscribe_key, None)
207
-
208
-    def test_can_specify_an_alternative_email_address(self):
209
-        # Test creating a notification with an email address that is different
210
-        # from the user's account email. This should set the account email
211
-        # address instead of the provided email in POST data.
212
-        page = self.get(reverse('catalogue:detail', args=(self.product.slug,
213
-                                                          self.product.id)))
214
-        notification_form = page.forms[1]
215
-        notification_form['email'] = 'someother@oscar.com'
216
-        page = notification_form.submit().follow()
217
-
218
-        self.assertContains(page, 'notified', status_code=200)
219
-
220
-        self.assertEquals(self.user.product_notifications.count(), 1)
221
-
222
-        notification = self.user.product_notifications.all()[0]
223
-        self.assertEquals(notification.product.id, self.product.id)
224
-        self.assertEquals(notification.get_notification_email(),
225
-                          self.user.email)
226
-        self.assertEquals(notification.confirm_key, None)
227
-        self.assertEquals(notification.unsubscribe_key, None)
228
-
229
-    def test_cannot_create_duplicate_notifications(self):
230
-        # Test creating a notification when the user has already signed up for
231
-        # this product notification. The user should be redirected to the product
232
-        # page with a notification that he has already signed up.
233
-        get(ProductNotification, product=self.product, user=self.user)
234
-
235
-        page = self.get(reverse('catalogue:detail', args=(self.product.slug,
236
-                                                          self.product.id)))
237
-        self.assertContains(
238
-            page,
239
-            "You will be notified when this product is available",
240
-        )
241
-
242
-
243
-class TestAnAnonymousButExistingUserRequestingANotification(NotificationWebTest):
244
-    is_anonymous = True
245
-    email = 'testuser@oscar.com'
246
-    username = 'testuser'
247
-    password = 'somerandompassword'
248
-
249
-    def setUp(self):
250
-        super(TestAnAnonymousButExistingUserRequestingANotification, self).setUp()
251
-        User.objects.create(username=self.username, password=self.password,
252
-                            email=self.email)
253
-        self.create_product_class()
254
-        self.product = self.create_product()
255
-
256
-    def test_gets_redirected_to_login_page(self):
257
-        # Test creating a notification when a registered user is not yet logged
258
-        # in. The email address in the form is checked against all users. If a
259
-        # user profile has this email address set, the user will be redirected
260
-        # to the login page and from there right back to the product detail
261
-        # page where the user hits the 'Notify Me' button again.
262
-        product_url = reverse('catalogue:detail', args=(self.product.slug,
263
-                                                        self.product.id))
264
-        page = self.app.get(product_url)
265
-        notification_form = page.forms[1]
266
-        notification_form['email'] = self.email
267
-        page = notification_form.submit()
268
-
269
-        redirect_url = "%s?next=%s" % (reverse('customer:login'), product_url)
270
-        self.assertRedirects(page, redirect_url)
271
-
272
-
273
-class TestASignedInUser(NotificationWebTest):
274
-    is_anonymous = False
275
-    email = 'testuser@oscar.com'
276
-    username = 'testuser'
277
-    password = 'password'
278
-
279
-    def setUp(self):
280
-        super(TestASignedInUser, self).setUp()
281
-        self.create_product_class()
282
-        self.product = self.create_product()
283
-        self.notification = ProductNotification.objects.create(
284
-            user=self.user,
285
-            product=self.product,
286
-            status=ProductNotification.ACTIVE)
287
-
288
-    def test_gets_a_404_when_accessing_invalid_url(self):
289
-        self.assertEquals(self.notification.status, ProductNotification.ACTIVE)
290
-
291
-        try:
292
-            self.get('/products/40-2/notify-me/set-status/1/invalid/')
293
-            self.fail('expected 404 but did not happen')
294
-        except AppError:
295
-            pass
296
-
297
-    def test_can_deactivate_a_notification_from_the_account_section(self):
298
-        self.assertEquals(self.notification.status, ProductNotification.ACTIVE)
299
-
300
-        notification_tab_url = reverse('customer:summary')+"?tab=notifications"
301
-        page = self.get(notification_tab_url)
302
-        notification_form = page.forms[1]
303
-        page = notification_form.submit('deactivate', index=0)
304
-
305
-        self.assertRedirects(page, notification_tab_url)
306
-
307
-        notification = ProductNotification.objects.get(id=self.notification.id)
308
-        self.assertEquals(notification.status, ProductNotification.INACTIVE)

+ 77
- 0
tests/functional/customer/alert_tests.py Vedi File

@@ -0,0 +1,77 @@
1
+from django_webtest import WebTest
2
+from django.contrib.auth.models import User
3
+from django.core.urlresolvers import reverse
4
+from django.core import mail
5
+from django_dynamic_fixture import G
6
+
7
+from oscar.test.helpers import create_product
8
+from oscar.apps.customer.models import ProductAlert
9
+
10
+
11
+class TestAUser(WebTest):
12
+
13
+    def test_can_create_a_stock_alert(self):
14
+        user = G(User)
15
+        product = create_product(num_in_stock=0)
16
+        product_page = self.app.get(product.get_absolute_url(), user=user)
17
+        form = product_page.forms['alert_form']
18
+        form.submit()
19
+
20
+        alerts = ProductAlert.objects.filter(user=user)
21
+        self.assertEqual(1, len(alerts))
22
+        alert = alerts[0]
23
+        self.assertEqual(ProductAlert.ACTIVE, alert.status)
24
+        self.assertEqual(alert.product, product)
25
+
26
+
27
+class TestAUserWithAnActiveStockAlert(WebTest):
28
+
29
+    def setUp(self):
30
+        self.user = G(User)
31
+        self.product = create_product(num_in_stock=0)
32
+        product_page = self.app.get(self.product.get_absolute_url(),
33
+                                    user=self.user)
34
+        form = product_page.forms['alert_form']
35
+        form.submit()
36
+
37
+    def test_can_cancel_it(self):
38
+        account_page = self.app.get(reverse('customer:summary'),
39
+                                    user=self.user)
40
+        form = account_page.forms['alerts_form']
41
+        form.submit('cancel_alert')
42
+
43
+        alerts = ProductAlert.objects.filter(user=self.user)
44
+        self.assertEqual(1, len(alerts))
45
+        alert = alerts[0]
46
+        self.assertTrue(alert.is_cancelled)
47
+
48
+    def test_gets_notified_when_it_is_back_in_stock(self):
49
+        self.product.stockrecord.num_in_stock = 10
50
+        self.product.stockrecord.save()
51
+        self.assertEqual(1, self.user.notifications.all().count())
52
+
53
+    def test_gets_emailed_when_it_is_back_in_stock(self):
54
+        self.product.stockrecord.num_in_stock = 10
55
+        self.product.stockrecord.save()
56
+        self.assertEqual(1, len(mail.outbox))
57
+
58
+    def test_does_not_get_emailed_when_it_is_saved_but_still_zero_stock(self):
59
+        self.product.stockrecord.num_in_stock = 0
60
+        self.product.stockrecord.save()
61
+        self.assertEqual(0, len(mail.outbox))
62
+
63
+
64
+class TestAnAnonymousUser(WebTest):
65
+
66
+    def test_can_create_a_stock_alert(self):
67
+        product = create_product(num_in_stock=0)
68
+        product_page = self.app.get(product.get_absolute_url())
69
+        form = product_page.forms['alert_form']
70
+        form['email'] = 'john@smith.com'
71
+        form.submit()
72
+
73
+        alerts = ProductAlert.objects.filter(email='john@smith.com')
74
+        self.assertEqual(1, len(alerts))
75
+        alert = alerts[0]
76
+        self.assertEqual(ProductAlert.UNCONFIRMED, alert.status)
77
+        self.assertEqual(alert.product, product)

+ 0
- 49
tests/functional/customer/notification_tests.py Vedi File

@@ -1,10 +1,7 @@
1 1
 from django.contrib.auth.models import User
2
-from django.core.urlresolvers import reverse
3 2
 from django_dynamic_fixture import G
4 3
 
5 4
 from oscar.test import WebTestCase
6
-from oscar.apps.catalogue.models import Product
7
-from oscar.apps.catalogue.notification.models import ProductNotification
8 5
 from oscar.apps.customer.notifications import services
9 6
 
10 7
 
@@ -18,49 +15,3 @@ class TestAUserWithUnreadNotifications(WebTestCase):
18 15
         homepage = self.app.get('/', user=self.user)
19 16
         self.assertTrue('num_unread_notifications' in homepage.context)
20 17
         self.assertEqual(1, homepage.context['num_unread_notifications'])
21
-
22
-
23
-class TestChangingNotificationStatusByUser(WebTestCase):
24
-
25
-    def setUp(self):
26
-        self.user = User.objects.create(username='testuser',
27
-                                        password='something',
28
-                                        email='testuser@example.com')
29
-
30
-        product = G(Product)
31
-        self.notification1 = ProductNotification.objects.create(user=self.user,
32
-                                                               product=product,
33
-                                                               status='active')
34
-        self.notification2 = ProductNotification.objects.create(user=self.user,
35
-                                                               product=product,
36
-                                                               status='inactive')
37
-
38
-    def test_deactivating_an_active_product_notification(self):
39
-        self.assertEquals(self.notification1.status, ProductNotification.ACTIVE)
40
-        self.assertEquals(self.notification2.status, ProductNotification.INACTIVE)
41
-
42
-        page = self.app.get(reverse("customer:summary"),
43
-                            params={'tab': 'notification'}, user=self.user)
44
-        notification_form = page.forms[1]
45
-        notification_form.submit('deactivate', index=0)
46
-
47
-        notification = ProductNotification.objects.get(pk=self.notification1.id)
48
-        self.assertEquals(notification.status, ProductNotification.INACTIVE)
49
-
50
-        notification = ProductNotification.objects.get(pk=self.notification2.id)
51
-        self.assertEquals(notification.status, ProductNotification.INACTIVE)
52
-
53
-    def test_activating_an_inactive_product_notification(self):
54
-        self.assertEquals(self.notification1.status, ProductNotification.ACTIVE)
55
-        self.assertEquals(self.notification2.status, ProductNotification.INACTIVE)
56
-
57
-        page = self.app.get(reverse("customer:summary"),
58
-                            params={'tab': 'notification'}, user=self.user)
59
-        notification_form = page.forms[1]
60
-        notification_form.submit('activate', index=0)
61
-
62
-        notification = ProductNotification.objects.get(pk=self.notification2.id)
63
-        self.assertEquals(notification.status, ProductNotification.ACTIVE)
64
-
65
-        notification = ProductNotification.objects.get(pk=self.notification1.id)
66
-        self.assertEquals(notification.status, ProductNotification.ACTIVE)

+ 18
- 0
tests/unit/customer/alert_tests.py Vedi File

@@ -0,0 +1,18 @@
1
+from django.test import TestCase
2
+from django.contrib.auth.models import User
3
+from django_dynamic_fixture import G
4
+
5
+from oscar.apps.customer.models import ProductAlert
6
+from oscar.test.helpers import create_product
7
+
8
+
9
+class TestAnAlertForARegisteredUser(TestCase):
10
+
11
+    def setUp(self):
12
+        user = G(User)
13
+        product = create_product()
14
+        self.alert = ProductAlert.objects.create(user=user,
15
+                                            product=product)
16
+
17
+    def test_defaults_to_active(self):
18
+        self.assertTrue(self.alert.is_active)

+ 0
- 119
tests/unit/notification_tests.py Vedi File

@@ -1,119 +0,0 @@
1
-from django.core import mail
2
-from django.test import TestCase
3
-from django.contrib.auth.models import User
4
-from django_dynamic_fixture import get
5
-
6
-from oscar.apps.catalogue.models import Product, ProductClass
7
-from oscar.apps.partner.models import StockRecord
8
-from oscar.apps.catalogue.notification.models import ProductNotification
9
-
10
-
11
-class TestANotificationForARegisteredUser(TestCase):
12
-
13
-    def setUp(self):
14
-        self.product = get(Product)
15
-        self.user = get(User)
16
-        self.notification = ProductNotification.objects.create(
17
-            user=self.user,
18
-            product=self.product)
19
-
20
-    def test_uses_the_users_email_for_notifications(self):
21
-        self.assertEquals(self.notification.get_notification_email(),
22
-                          self.user.email)
23
-
24
-    def test_defaults_to_inactive_status(self):
25
-        self.assertFalse(self.notification.is_active())
26
-
27
-    def test_defaults_to_unconfirmed(self):
28
-        self.assertFalse(self.notification.is_confirmed())
29
-
30
-
31
-class TestANotificationForAnAnonymousUser(TestCase):
32
-
33
-    def setUp(self):
34
-        self.product = get(Product)
35
-        self.email = 'test@oscarcommerce.com'
36
-        self.notification = ProductNotification.objects.create(
37
-            email=self.email,
38
-            product=self.product)
39
-
40
-    def test_uses_the_specified_email_for_notifications(self):
41
-        self.assertEquals(self.notification.get_notification_email(),
42
-                          self.email)
43
-
44
-
45
-class TestNotificationEmails(TestCase):
46
-
47
-    def setUp(self):
48
-        self.product_class = ProductClass.objects.create(name='books')
49
-        self.product = get(Product, product_class=self.product_class,
50
-                           title='product', upc='000000000001')
51
-        self.reference_product = get(Product, product_class=self.product_class,
52
-                           title='reference product', upc='000000000002')
53
-
54
-        self.first_user = get(User, email='firstuser@one.com')
55
-        self.second_user = get(User, email='seconduser@two.com')
56
-
57
-        get(StockRecord, product=self.product, num_in_stock=0)
58
-
59
-    def test_are_not_sent_when_stock_level_remains_0(self):
60
-        stock_record = StockRecord.objects.get(id=1)
61
-        self.assertEquals(stock_record.num_in_stock, 0)
62
-        stock_record.save()
63
-        self.assertEquals(stock_record.num_in_stock, 0)
64
-
65
-        self.assertEquals(len(mail.outbox), 0)
66
-
67
-    def test_are_not_sent_when_there_are_no_notifications(self):
68
-        stock_record = StockRecord.objects.get(id=1)
69
-        self.assertEquals(stock_record.num_in_stock, 0)
70
-
71
-        stock_record.num_in_stock = 20
72
-        stock_record.save()
73
-
74
-        self.assertEquals(len(mail.outbox), 0)
75
-
76
-    def test_are_sent_correctly_when_there_are_notifications(self):
77
-        ProductNotification.objects.create(user=self.first_user,
78
-                                           product=self.product,
79
-                                           status=ProductNotification.ACTIVE)
80
-        ProductNotification.objects.create(user=self.second_user,
81
-                                           product=self.product,
82
-                                           status=ProductNotification.ACTIVE)
83
-        ProductNotification.objects.create(user=None,
84
-                                           email='anonymous@test.com',
85
-                                           product=self.product,
86
-                                           status=ProductNotification.ACTIVE)
87
-        ProductNotification.objects.create(user=self.second_user,
88
-                                           product=self.reference_product,
89
-                                           status=ProductNotification.ACTIVE)
90
-        ProductNotification.objects.create(product=self.product,
91
-                                           status=ProductNotification.INACTIVE)
92
-
93
-        stock_record = StockRecord.objects.get(id=1)
94
-        self.assertEquals(stock_record.num_in_stock, 0)
95
-
96
-        stock_record.num_in_stock = 20
97
-        stock_record.save()
98
-
99
-        self.assertEquals(len(mail.outbox), 3)
100
-        self.assertItemsEqual(
101
-            [e.to[0] for e in mail.outbox],
102
-            [self.first_user.email, self.second_user.email, 'anonymous@test.com'],
103
-        )
104
-
105
-        notification = ProductNotification.objects.get(id=1)
106
-        self.assertEquals(notification.status, ProductNotification.INACTIVE)
107
-        self.assertNotEquals(notification.date_notified, None)
108
-
109
-        notification = ProductNotification.objects.get(id=2)
110
-        self.assertEquals(notification.status, ProductNotification.INACTIVE)
111
-        self.assertNotEquals(notification.date_notified, None)
112
-
113
-        notification = ProductNotification.objects.get(id=3)
114
-        self.assertEquals(notification.status, ProductNotification.INACTIVE)
115
-        self.assertNotEquals(notification.date_notified, None)
116
-
117
-        notification = ProductNotification.objects.get(id=4)
118
-        self.assertEquals(notification.status, ProductNotification.ACTIVE)
119
-        self.assertEquals(notification.date_notified, None)

Loading…
Annulla
Salva