Bläddra i källkod

Merge branch 'master' into feature/variants_dashboard

This mostly pulls in the tables2 work. I opted to just use master's
branch product_list template, and will add the variants column that
I added as part of #1441 as a tables2 column.

Conflicts:
	oscar/apps/catalogue/abstract_models.py
	oscar/apps/dashboard/catalogue/views.py
	oscar/templates/oscar/dashboard/catalogue/product_list.html
master
Maik Hoepfel 11 år sedan
förälder
incheckning
7495405bb6
100 ändrade filer med 1366 tillägg och 1147 borttagningar
  1. 5
    3
      .travis.yml
  2. 3
    13
      README.rst
  3. 1
    1
      docs/source/howto/how_to_configure_shipping.rst
  4. 0
    20
      docs/source/ref/settings.rst
  5. 23
    4
      docs/source/releases/v0.8.rst
  6. 4
    0
      docs/source/topics/class_loading_explained.rst
  7. 1
    0
      oscar/__init__.py
  8. 15
    4
      oscar/apps/address/abstract_models.py
  9. 30
    30
      oscar/apps/address/migrations/0001_initial.py
  10. 7
    3
      oscar/apps/analytics/abstract_models.py
  11. 7
    7
      oscar/apps/analytics/migrations/0001_initial.py
  12. 16
    16
      oscar/apps/analytics/migrations/0002_auto_20140805_1510.py
  13. 5
    2
      oscar/apps/basket/abstract_models.py
  14. 6
    6
      oscar/apps/basket/migrations/0001_initial.py
  15. 13
    13
      oscar/apps/basket/migrations/0002_auto_20140805_1510.py
  16. 34
    38
      oscar/apps/basket/views.py
  17. 46
    15
      oscar/apps/catalogue/abstract_models.py
  18. 83
    83
      oscar/apps/catalogue/migrations/0001_initial.py
  19. 6
    3
      oscar/apps/catalogue/reviews/abstract_models.py
  20. 15
    15
      oscar/apps/catalogue/reviews/migrations/0001_initial.py
  21. 4
    5
      oscar/apps/catalogue/reviews/views.py
  22. 26
    26
      oscar/apps/checkout/mixins.py
  23. 1
    1
      oscar/apps/checkout/views.py
  24. 20
    9
      oscar/apps/customer/abstract_models.py
  25. 14
    3
      oscar/apps/customer/auth_backends.py
  26. 1
    1
      oscar/apps/customer/config.py
  27. 7
    13
      oscar/apps/customer/forms.py
  28. 5
    14
      oscar/apps/customer/history.py
  29. 30
    30
      oscar/apps/customer/migrations/0001_initial.py
  30. 19
    0
      oscar/apps/customer/migrations/0002_auto_20140808_1205.py
  31. 1
    1
      oscar/apps/customer/models.py
  32. 4
    6
      oscar/apps/customer/notifications/views.py
  33. 17
    0
      oscar/apps/customer/receivers.py
  34. 4
    4
      oscar/apps/customer/utils.py
  35. 15
    19
      oscar/apps/customer/views.py
  36. 14
    17
      oscar/apps/customer/wishlists/views.py
  37. 58
    0
      oscar/apps/dashboard/catalogue/tables.py
  38. 37
    16
      oscar/apps/dashboard/catalogue/views.py
  39. 8
    5
      oscar/apps/dashboard/communications/forms.py
  40. 4
    2
      oscar/apps/dashboard/communications/views.py
  41. 4
    3
      oscar/apps/dashboard/offers/forms.py
  42. 1
    3
      oscar/apps/dashboard/orders/views.py
  43. 7
    0
      oscar/apps/dashboard/tables.py
  44. 25
    0
      oscar/apps/dashboard/users/tables.py
  45. 40
    15
      oscar/apps/dashboard/users/views.py
  46. 1
    1
      oscar/apps/dashboard/views.py
  47. 1
    1
      oscar/apps/offer/admin.py
  48. 6
    18
      oscar/apps/offer/custom.py
  49. 52
    52
      oscar/apps/offer/migrations/0001_initial.py
  50. 66
    46
      oscar/apps/offer/models.py
  51. 30
    15
      oscar/apps/order/abstract_models.py
  52. 1
    10
      oscar/apps/order/admin.py
  53. 119
    119
      oscar/apps/order/migrations/0001_initial.py
  54. 2
    2
      oscar/apps/order/processing.py
  55. 7
    3
      oscar/apps/partner/abstract_models.py
  56. 38
    38
      oscar/apps/partner/migrations/0001_initial.py
  57. 9
    4
      oscar/apps/payment/abstract_models.py
  58. 1
    1
      oscar/apps/payment/bankcards.py
  59. 4
    5
      oscar/apps/payment/forms.py
  60. 26
    26
      oscar/apps/payment/migrations/0001_initial.py
  61. 0
    8
      oscar/apps/promotions/admin.py
  62. 49
    49
      oscar/apps/promotions/migrations/0001_initial.py
  63. 13
    6
      oscar/apps/promotions/models.py
  64. 1
    1
      oscar/apps/search/facets.py
  65. 5
    2
      oscar/apps/shipping/abstract_models.py
  66. 15
    2
      oscar/apps/shipping/methods.py
  67. 21
    21
      oscar/apps/shipping/migrations/0001_initial.py
  68. 5
    2
      oscar/apps/voucher/abstract_models.py
  69. 13
    13
      oscar/apps/voucher/migrations/0001_initial.py
  70. 6
    3
      oscar/apps/wishlists/abstract_models.py
  71. 13
    13
      oscar/apps/wishlists/migrations/0001_initial.py
  72. 1
    1
      oscar/core/ajax.py
  73. 1
    3
      oscar/core/application.py
  74. 1
    1
      oscar/core/compat.py
  75. 1
    1
      oscar/core/context_processors.py
  76. 14
    5
      oscar/core/loading.py
  77. 5
    6
      oscar/core/phonenumber.py
  78. 8
    0
      oscar/core/prices.py
  79. 32
    1
      oscar/core/utils.py
  80. 0
    3
      oscar/defaults.py
  81. 5
    5
      oscar/forms/widgets.py
  82. 3
    1
      oscar/management/commands/oscar_fork_app.py
  83. 0
    2
      oscar/management/commands/oscar_fork_statics.py
  84. 0
    1
      oscar/management/commands/oscar_generate_email_content.py
  85. 3
    4
      oscar/models/fields/__init__.py
  86. 1
    1
      oscar/models/fields/autoslugfield.py
  87. 0
    2
      oscar/profiling/decorators.py
  88. 0
    2
      oscar/profiling/middleware.py
  89. 6
    0
      oscar/static/oscar/css/dashboard.css
  90. 0
    6
      oscar/static/oscar/css/styles.css
  91. 2
    1
      oscar/static/oscar/js/oscar/ui.js
  92. 6
    0
      oscar/static/oscar/less/dashboard.less
  93. 4
    4
      oscar/templates/oscar/500.html
  94. 17
    14
      oscar/templates/oscar/basket/partials/basket_content.html
  95. 4
    59
      oscar/templates/oscar/dashboard/catalogue/category_list.html
  96. 37
    0
      oscar/templates/oscar/dashboard/catalogue/category_row_actions.html
  97. 2
    1
      oscar/templates/oscar/dashboard/catalogue/messages/product_saved.html
  98. 2
    93
      oscar/templates/oscar/dashboard/catalogue/product_list.html
  99. 21
    0
      oscar/templates/oscar/dashboard/catalogue/product_row_actions.html
  100. 0
    0
      oscar/templates/oscar/dashboard/catalogue/product_row_image.html

+ 5
- 3
.travis.yml Visa fil

@@ -11,12 +11,14 @@ env:
11 11
     secure: FuIlzEsGJiAwhaIRBmRNsq9eXmuzs25fX6BChknW4lDyVAySWMp0+Zps9Bd0JgfFYUG3Ip+OTmksYIoTUsG25ZJS9cq1IFt3QKUAN70YCI/4ZBLeIdICPEyxq+Km179+NeEXmBUug17RLMLxh3MWfO+RKUHK9yHIPNNpq0dNyoo=
12 12
   matrix:
13 13
     - DJANGO=Django==1.6.5
14
-    - DJANGO=https://www.djangoproject.com/download/1.7c2/tarball/
14
+    - DJANGO=https://www.djangoproject.com/download/1.7c2/tarball/#egg=Django
15 15
 
16 16
 matrix:
17 17
   allow_failures:
18
-    - python: '3.3'
19
-    - python: '3.4'
18
+    - env: "DJANGO=Django==1.6.5"
19
+      python: '3.3'
20
+    - env: "DJANGO=Django==1.6.5"
21
+      python: '3.4'
20 22
 
21 23
 install:
22 24
   - easy_install $DJANGO

+ 3
- 13
README.rst Visa fil

@@ -175,6 +175,7 @@ The following are community-written extensions:
175 175
 * django-oscar-unicredit_ - Integration with the Unicredit payment gateway
176 176
 * django-oscar-payments_ - Pluggable payments for Oscar
177 177
 * django-oscar-recurly_ - Integration with the Recurly payment gateway
178
+* django-oscar-adyen_ - Integration with the Adyen payment gateway
178 179
 * oscar-sagepay_ - Payment integration with Sage Pay
179 180
 * django-oscar-erp_
180 181
 
@@ -184,6 +185,7 @@ Let us know if you're writing a new one!
184 185
 .. _django-oscar-erp: https://bitbucket.org/zikzakmedia/django-oscar_erp
185 186
 .. _django-oscar-payments: https://github.com/Lacrymology/django-oscar-payments
186 187
 .. _django-oscar-recurly: https://github.com/mynameisgabe/django-oscar-recurly
188
+.. _django-oscar-adyen: https://github.com/oscaro/django-oscar-adyen
187 189
 .. _oscar-sagepay: https://github.com/udox/oscar-sagepay
188 190
 
189 191
 License
@@ -236,9 +238,6 @@ Selected Tangent projects:
236 238
 Non-Tangent:
237 239
 
238 240
 * Dolbeau - http://www.dolbeau.ca
239
-* Sobusa - http://www.sobusa.fr
240
-* Laivee - http://laivee.pl
241
-* Colinss - http://colinss.com
242 241
 * Audio App - https://audioapp.pl
243 242
 * Anything Gift - http://www.anythinggift.co.uk
244 243
 * FP Sport - http://www.fpsport.it
@@ -250,15 +249,6 @@ Non-Tangent:
250 249
 .. image:: https://github.com/tangentlabs/django-oscar/raw/master/docs/images/screenshots/dolbeau.thumb.png
251 250
     :target: http://www.dolbeau.ca
252 251
 
253
-.. image:: https://github.com/tangentlabs/django-oscar/raw/master/docs/images/screenshots/sobusa.thumb.png
254
-    :target: http://www.sobusa.fr
255
-
256
-.. image:: https://github.com/tangentlabs/django-oscar/raw/master/docs/images/screenshots/laivee.thumb.png
257
-    :target: http://www.laivee.pl
258
-
259
-.. image:: https://github.com/tangentlabs/django-oscar/raw/master/docs/images/screenshots/colinss.thumb.png
260
-    :target: http://www.colinss.com
261
-
262 252
 .. image:: https://github.com/tangentlabs/django-oscar/raw/master/docs/images/screenshots/audioapp.thumb.png
263 253
     :target: https://audioapp.pl
264 254
 
@@ -266,7 +256,7 @@ Non-Tangent:
266 256
     :target: http://www.anythinggift.co.uk
267 257
 
268 258
 .. image:: https://github.com/tangentlabs/django-oscar/raw/master/docs/images/screenshots/fpsport.thumb.png
269
-    :target: https://www.fpsport.it
259
+    :target: http://www.fpsport.it
270 260
 
271 261
 .. image:: https://github.com/tangentlabs/django-oscar/raw/master/docs/images/screenshots/garmsby.thumb.png
272 262
     :target: https://garmsby.co.uk

+ 1
- 1
docs/source/howto/how_to_configure_shipping.rst Visa fil

@@ -85,7 +85,7 @@ For more complex logic, override the ``get_available_shipping_methods`` method:
85 85
                self, basket, user=None, shipping_addr=None, 
86 86
                request=None, **kwargs):
87 87
            methods = (methods.Standard())
88
-           if shipping_addr and shipping.addr.country.code == 'GB':
88
+           if shipping_addr and shipping_addr.country.code == 'GB':
89 89
                # Express is only available in the UK
90 90
                methods = (methods.Standard(), methods.Express())
91 91
            return methods

+ 0
- 20
docs/source/ref/settings.rst Visa fil

@@ -312,26 +312,6 @@ used in Oscar's default templates but could be used to include static assets
312 312
 Offer settings
313 313
 ==============
314 314
 
315
-``OSCAR_OFFER_BLACKLIST_PRODUCT``
316
----------------------------------
317
-
318
-Default: ``None``
319
-
320
-A function which takes a product as its sole parameter and returns a boolean
321
-indicating if the product is blacklisted from offers or not.
322
-
323
-Example::
324
-
325
-    from decimal import Decimal as D
326
-
327
-    def is_expensive(product):
328
-        if product.has_stockrecord:
329
-            return product.stockrecord.price_incl_tax > D('1000.00')
330
-        return False
331
-
332
-    # Don't allow expensive products to be in offers
333
-    OSCAR_OFFER_BLACKLIST_PRODUCT = is_expensive
334
-
335 315
 ``OSCAR_OFFER_ROUNDING_FUNCTION``
336 316
 ---------------------------------
337 317
 

+ 23
- 4
docs/source/releases/v0.8.rst Visa fil

@@ -37,11 +37,12 @@ availability logic now needs to be handled with strategies.
37 37
 Compatibility
38 38
 -------------
39 39
 
40
-Oscar 0.8 drops supports for Django 1.5, and is compatible with Django 1.6 and
41
-Django 1.7.
40
+This release adds support for Django 1.7. Per our policy of always supporting
41
+two versions of Django, support for Django 1.5 has been dropped.
42 42
 
43
-Support for Python 2.6 has been dropped; Oscar works with Python 2.7, 3.3
44
-and 3.4.
43
+This release also adds full Python 3.3 and 3.4 support. But due to South
44
+not supporting Python 3, Python 3 support is only supported in combination
45
+with Django 1.7 and it's new migration framework.
45 46
 
46 47
 .. _new_in_0.8:
47 48
 
@@ -232,6 +233,12 @@ Minor changes
232 233
   expected. There's currently no frontend or dashboard support for it, as there
233 234
   is no good default behaviour.
234 235
 
236
+* Oscar has a new dependency, django-tables2_. It's a handy library that helps
237
+  when displaying tabular data, allowing sorting, etc. It also makes it easier
238
+  to adapt e.g. the product list view in the dashboard to additional fields.
239
+
240
+.. _django-tables2: http://django-tables2.readthedocs.org/en/latest/
241
+
235 242
 .. _incompatible_changes_in_0.8:
236 243
 
237 244
 Backwards incompatible changes in 0.8
@@ -493,6 +500,18 @@ Misc
493 500
   ``ImportError`` if a model can't be found. That brings it more in line with
494 501
   what Django does since the app refactor.
495 502
 
503
+* ``CommunicationEventType.category`` was storing a localised string, which
504
+  breaks when switching locale. It now uses ``choices`` to map between the
505
+  value and a localised string. Unfortunately, if you're using this feature
506
+  and not running an English locale, you will need to migrate the existing
507
+  data to the English values.
508
+
509
+* Support for the ``OSCAR_OFFER_BLACKLIST_PRODUCT`` setting has been removed.
510
+  It was only partially supported: it prevented products from being
511
+  added to a range, but offers could be applied to the products nonetheless.
512
+  To prevent an offer being applied to a product, use ``is_discountable`` or
513
+  override ``get_is_discountable`` on your product instances.
514
+
496 515
 .. _rewritten: https://github.com/tangentlabs/django-oscar/commit/d8b4dbfed17be90846ea4bc47b5f7b39ad944c24
497 516
 
498 517
 Basket line stockrecords

+ 4
- 0
docs/source/topics/class_loading_explained.rst Visa fil

@@ -64,6 +64,10 @@ use ``get_class`` when importing classes from Oscar. This means that if someday
64 64
 the class is overridden, it will not require code changes. Care should be taken
65 65
 when doing this, as this is a tricky trade-off between maintainability and
66 66
 added complexity.
67
+Please note that we cannot recommend ever using ``get_model`` in your own code.
68
+Especially pre-Django 1.7, model initialisation is a tricky process and it's
69
+easy to run into circular import issues.
70
+
67 71
 
68 72
 Testing
69 73
 -------

+ 1
- 0
oscar/__init__.py Visa fil

@@ -64,6 +64,7 @@ OSCAR_CORE_APPS = [
64 64
     'haystack',
65 65
     'treebeard',
66 66
     'sorl.thumbnail',
67
+    'django_tables2',
67 68
 ]
68 69
 
69 70
 

+ 15
- 4
oscar/apps/address/abstract_models.py Visa fil

@@ -2,14 +2,16 @@ import re
2 2
 import zlib
3 3
 
4 4
 from django.db import models
5
+from django.utils.encoding import python_2_unicode_compatible
5 6
 from django.utils.translation import ugettext_lazy as _, pgettext_lazy
6 7
 from django.core import exceptions
7 8
 
8 9
 from oscar.core.compat import AUTH_USER_MODEL
9 10
 from oscar.models.fields import UppercaseCharField, PhoneNumberField
10
-from six.moves import filter
11
+from django.utils.six.moves import filter
11 12
 
12 13
 
14
+@python_2_unicode_compatible
13 15
 class AbstractAddress(models.Model):
14 16
     """
15 17
     Superclass address object
@@ -233,7 +235,7 @@ class AbstractAddress(models.Model):
233 235
     search_text = models.TextField(
234 236
         _("Search text - used only for searching addresses"), editable=False)
235 237
 
236
-    def __unicode__(self):
238
+    def __str__(self):
237 239
         return self.summary
238 240
 
239 241
     class Meta:
@@ -373,6 +375,7 @@ class AbstractAddress(models.Model):
373 375
         return fields
374 376
 
375 377
 
378
+@python_2_unicode_compatible
376 379
 class AbstractCountry(models.Model):
377 380
     """
378 381
     International Organization for Standardization (ISO) 3166-1 Country list.
@@ -407,7 +410,7 @@ class AbstractCountry(models.Model):
407 410
         verbose_name_plural = _('Countries')
408 411
         ordering = ('-display_order', 'printable_name',)
409 412
 
410
-    def __unicode__(self):
413
+    def __str__(self):
411 414
         return self.printable_name or self.name
412 415
 
413 416
     @property
@@ -436,7 +439,16 @@ class AbstractShippingAddress(AbstractAddress):
436 439
 
437 440
     A shipping address should not be edited once the order has been placed -
438 441
     it should be read-only after that.
442
+
443
+    NOTE:
444
+    ShippingAddress is a model of the order app. But moving it there is tricky
445
+    due to circular import issues that are amplified by get_model/get_class
446
+    calls pre-Django 1.7 to register receivers. So...
447
+    TODO: Once Django 1.6 support is dropped, move AbstractBillingAddress and
448
+    AbstractShippingAddress to the order app, and move
449
+    PartnerAddress to the partner app.
439 450
     """
451
+
440 452
     phone_number = PhoneNumberField(
441 453
         _("Phone number"), blank=True,
442 454
         help_text=_("In case we need to call you about your order"))
@@ -541,7 +553,6 @@ class AbstractUserAddress(AbstractShippingAddress):
541 553
 
542 554
 
543 555
 class AbstractBillingAddress(AbstractAddress):
544
-
545 556
     class Meta:
546 557
         abstract = True
547 558
         # BillingAddress is registered in order/models.py

+ 30
- 30
oscar/apps/address/migrations/0001_initial.py Visa fil

@@ -16,56 +16,56 @@ class Migration(migrations.Migration):
16 16
         migrations.CreateModel(
17 17
             name='Country',
18 18
             fields=[
19
-                ('iso_3166_1_a2', models.CharField(max_length=2, serialize=False, verbose_name='ISO 3166-1 alpha-2', primary_key=True)),
20
-                ('iso_3166_1_a3', models.CharField(max_length=3, verbose_name='ISO 3166-1 alpha-3', blank=True)),
21
-                ('iso_3166_1_numeric', models.CharField(max_length=3, verbose_name='ISO 3166-1 numeric', blank=True)),
22
-                ('printable_name', models.CharField(max_length=128, verbose_name='Country name')),
23
-                ('name', models.CharField(max_length=128, verbose_name='Official name')),
24
-                ('display_order', models.PositiveSmallIntegerField(default=0, help_text='Higher the number, higher the country in the list.', verbose_name='Display order', db_index=True)),
25
-                ('is_shipping_country', models.BooleanField(default=False, db_index=True, verbose_name='Is shipping country')),
19
+                ('iso_3166_1_a2', models.CharField(verbose_name='ISO 3166-1 alpha-2', primary_key=True, serialize=False, max_length=2)),
20
+                ('iso_3166_1_a3', models.CharField(verbose_name='ISO 3166-1 alpha-3', blank=True, max_length=3)),
21
+                ('iso_3166_1_numeric', models.CharField(verbose_name='ISO 3166-1 numeric', blank=True, max_length=3)),
22
+                ('printable_name', models.CharField(verbose_name='Country name', max_length=128)),
23
+                ('name', models.CharField(verbose_name='Official name', max_length=128)),
24
+                ('display_order', models.PositiveSmallIntegerField(verbose_name='Display order', help_text='Higher the number, higher the country in the list.', db_index=True, default=0)),
25
+                ('is_shipping_country', models.BooleanField(verbose_name='Is shipping country', default=False, db_index=True)),
26 26
             ],
27 27
             options={
28
-                'ordering': (b'-display_order', b'printable_name'),
29
-                'abstract': False,
30 28
                 'verbose_name': 'Country',
31 29
                 'verbose_name_plural': 'Countries',
30
+                'ordering': ('-display_order', 'printable_name'),
31
+                'abstract': False,
32 32
             },
33 33
             bases=(models.Model,),
34 34
         ),
35 35
         migrations.CreateModel(
36 36
             name='UserAddress',
37 37
             fields=[
38
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
39
-                ('title', models.CharField(blank=True, max_length=64, verbose_name='Title', choices=[(b'Mr', 'Mr'), (b'Miss', 'Miss'), (b'Mrs', 'Mrs'), (b'Ms', 'Ms'), (b'Dr', 'Dr')])),
40
-                ('first_name', models.CharField(max_length=255, verbose_name='First name', blank=True)),
41
-                ('last_name', models.CharField(max_length=255, verbose_name='Last name', blank=True)),
42
-                ('line1', models.CharField(max_length=255, verbose_name='First line of address')),
43
-                ('line2', models.CharField(max_length=255, verbose_name='Second line of address', blank=True)),
44
-                ('line3', models.CharField(max_length=255, verbose_name='Third line of address', blank=True)),
45
-                ('line4', models.CharField(max_length=255, verbose_name='City', blank=True)),
46
-                ('state', models.CharField(max_length=255, verbose_name='State/County', blank=True)),
47
-                ('postcode', oscar.models.fields.UppercaseCharField(max_length=64, verbose_name='Post/Zip-code', blank=True)),
48
-                ('search_text', models.TextField(verbose_name='Search text - used only for searching addresses', editable=False)),
49
-                ('phone_number', oscar.models.fields.PhoneNumberField(help_text='In case we need to call you about your order', verbose_name='Phone number', blank=True)),
50
-                ('notes', models.TextField(help_text='Tell us anything we should know when delivering your order.', verbose_name='Instructions', blank=True)),
51
-                ('is_default_for_shipping', models.BooleanField(default=False, verbose_name='Default shipping address?')),
52
-                ('is_default_for_billing', models.BooleanField(default=False, verbose_name='Default billing address?')),
53
-                ('num_orders', models.PositiveIntegerField(default=0, verbose_name='Number of Orders')),
54
-                ('hash', models.CharField(verbose_name='Address Hash', max_length=255, editable=False, db_index=True)),
55
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
38
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
39
+                ('title', models.CharField(verbose_name='Title', choices=[('Mr', 'Mr'), ('Miss', 'Miss'), ('Mrs', 'Mrs'), ('Ms', 'Ms'), ('Dr', 'Dr')], blank=True, max_length=64)),
40
+                ('first_name', models.CharField(verbose_name='First name', blank=True, max_length=255)),
41
+                ('last_name', models.CharField(verbose_name='Last name', blank=True, max_length=255)),
42
+                ('line1', models.CharField(verbose_name='First line of address', max_length=255)),
43
+                ('line2', models.CharField(verbose_name='Second line of address', blank=True, max_length=255)),
44
+                ('line3', models.CharField(verbose_name='Third line of address', blank=True, max_length=255)),
45
+                ('line4', models.CharField(verbose_name='City', blank=True, max_length=255)),
46
+                ('state', models.CharField(verbose_name='State/County', blank=True, max_length=255)),
47
+                ('postcode', oscar.models.fields.UppercaseCharField(verbose_name='Post/Zip-code', blank=True, max_length=64)),
48
+                ('search_text', models.TextField(editable=False, verbose_name='Search text - used only for searching addresses')),
49
+                ('phone_number', oscar.models.fields.PhoneNumberField(verbose_name='Phone number', blank=True, help_text='In case we need to call you about your order')),
50
+                ('notes', models.TextField(verbose_name='Instructions', blank=True, help_text='Tell us anything we should know when delivering your order.')),
51
+                ('is_default_for_shipping', models.BooleanField(verbose_name='Default shipping address?', default=False)),
52
+                ('is_default_for_billing', models.BooleanField(verbose_name='Default billing address?', default=False)),
53
+                ('num_orders', models.PositiveIntegerField(verbose_name='Number of Orders', default=0)),
54
+                ('hash', models.CharField(editable=False, verbose_name='Address Hash', db_index=True, max_length=255)),
55
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
56 56
                 ('country', models.ForeignKey(verbose_name='Country', to='address.Country')),
57 57
                 ('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL)),
58 58
             ],
59 59
             options={
60
-                'ordering': [b'-num_orders'],
61
-                'abstract': False,
62 60
                 'verbose_name': 'User address',
63 61
                 'verbose_name_plural': 'User addresses',
62
+                'ordering': ['-num_orders'],
63
+                'abstract': False,
64 64
             },
65 65
             bases=(models.Model,),
66 66
         ),
67 67
         migrations.AlterUniqueTogether(
68 68
             name='useraddress',
69
-            unique_together=set([(b'user', b'hash')]),
69
+            unique_together=set([('user', 'hash')]),
70 70
         ),
71 71
     ]

+ 7
- 3
oscar/apps/analytics/abstract_models.py Visa fil

@@ -1,10 +1,12 @@
1 1
 from decimal import Decimal
2 2
 
3 3
 from django.db import models
4
+from django.utils.encoding import python_2_unicode_compatible
4 5
 from django.utils.translation import ugettext_lazy as _
5 6
 from oscar.core.compat import AUTH_USER_MODEL
6 7
 
7 8
 
9
+@python_2_unicode_compatible
8 10
 class AbstractProductRecord(models.Model):
9 11
     """
10 12
     A record of a how popular a product is.
@@ -34,7 +36,7 @@ class AbstractProductRecord(models.Model):
34 36
         verbose_name = _('Product record')
35 37
         verbose_name_plural = _('Product records')
36 38
 
37
-    def __unicode__(self):
39
+    def __str__(self):
38 40
         return _("Record for '%s'") % self.product
39 41
 
40 42
 
@@ -70,6 +72,7 @@ class AbstractUserRecord(models.Model):
70 72
         verbose_name_plural = _('User records')
71 73
 
72 74
 
75
+@python_2_unicode_compatible
73 76
 class AbstractUserProductView(models.Model):
74 77
 
75 78
     user = models.ForeignKey(AUTH_USER_MODEL, verbose_name=_("User"))
@@ -82,11 +85,12 @@ class AbstractUserProductView(models.Model):
82 85
         verbose_name = _('User product view')
83 86
         verbose_name_plural = _('User product views')
84 87
 
85
-    def __unicode__(self):
88
+    def __str__(self):
86 89
         return _("%(user)s viewed '%(product)s'") % {
87 90
             'user': self.user, 'product': self.product}
88 91
 
89 92
 
93
+@python_2_unicode_compatible
90 94
 class AbstractUserSearch(models.Model):
91 95
 
92 96
     user = models.ForeignKey(AUTH_USER_MODEL, verbose_name=_("User"))
@@ -99,7 +103,7 @@ class AbstractUserSearch(models.Model):
99 103
         verbose_name = _("User search query")
100 104
         verbose_name_plural = _("User search queries")
101 105
 
102
-    def __unicode__(self):
106
+    def __str__(self):
103 107
         return _("%(user)s searched for '%(query)s'") % {
104 108
             'user': self.user,
105 109
             'query': self.query}

+ 7
- 7
oscar/apps/analytics/migrations/0001_initial.py Visa fil

@@ -13,17 +13,17 @@ class Migration(migrations.Migration):
13 13
         migrations.CreateModel(
14 14
             name='ProductRecord',
15 15
             fields=[
16
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
17
-                ('num_views', models.PositiveIntegerField(default=0, verbose_name='Views')),
18
-                ('num_basket_additions', models.PositiveIntegerField(default=0, verbose_name='Basket Additions')),
19
-                ('num_purchases', models.PositiveIntegerField(default=0, verbose_name='Purchases', db_index=True)),
20
-                ('score', models.FloatField(default=0.0, verbose_name='Score')),
16
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
17
+                ('num_views', models.PositiveIntegerField(verbose_name='Views', default=0)),
18
+                ('num_basket_additions', models.PositiveIntegerField(verbose_name='Basket Additions', default=0)),
19
+                ('num_purchases', models.PositiveIntegerField(verbose_name='Purchases', db_index=True, default=0)),
20
+                ('score', models.FloatField(verbose_name='Score', default=0.0)),
21 21
             ],
22 22
             options={
23
-                'ordering': [b'-num_purchases'],
24
-                'abstract': False,
25 23
                 'verbose_name': 'Product record',
26 24
                 'verbose_name_plural': 'Product records',
25
+                'ordering': ['-num_purchases'],
26
+                'abstract': False,
27 27
             },
28 28
             bases=(models.Model,),
29 29
         ),

oscar/apps/analytics/migrations/0002_auto_20140729_1113.py → oscar/apps/analytics/migrations/0002_auto_20140805_1510.py Visa fil

@@ -24,50 +24,50 @@ class Migration(migrations.Migration):
24 24
         migrations.CreateModel(
25 25
             name='UserProductView',
26 26
             fields=[
27
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
28
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
27
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
28
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
29 29
                 ('product', models.ForeignKey(verbose_name='Product', to='catalogue.Product')),
30 30
                 ('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL)),
31 31
             ],
32 32
             options={
33
-                'abstract': False,
34 33
                 'verbose_name': 'User product view',
35 34
                 'verbose_name_plural': 'User product views',
35
+                'abstract': False,
36 36
             },
37 37
             bases=(models.Model,),
38 38
         ),
39 39
         migrations.CreateModel(
40 40
             name='UserRecord',
41 41
             fields=[
42
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
43
-                ('num_product_views', models.PositiveIntegerField(default=0, verbose_name='Product Views')),
44
-                ('num_basket_additions', models.PositiveIntegerField(default=0, verbose_name='Basket Additions')),
45
-                ('num_orders', models.PositiveIntegerField(default=0, verbose_name='Orders', db_index=True)),
46
-                ('num_order_lines', models.PositiveIntegerField(default=0, verbose_name='Order Lines', db_index=True)),
47
-                ('num_order_items', models.PositiveIntegerField(default=0, verbose_name='Order Items', db_index=True)),
48
-                ('total_spent', models.DecimalField(default=Decimal('0.00'), verbose_name='Total Spent', max_digits=12, decimal_places=2)),
49
-                ('date_last_order', models.DateTimeField(null=True, verbose_name='Last Order Date', blank=True)),
42
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
43
+                ('num_product_views', models.PositiveIntegerField(verbose_name='Product Views', default=0)),
44
+                ('num_basket_additions', models.PositiveIntegerField(verbose_name='Basket Additions', default=0)),
45
+                ('num_orders', models.PositiveIntegerField(verbose_name='Orders', db_index=True, default=0)),
46
+                ('num_order_lines', models.PositiveIntegerField(verbose_name='Order Lines', db_index=True, default=0)),
47
+                ('num_order_items', models.PositiveIntegerField(verbose_name='Order Items', db_index=True, default=0)),
48
+                ('total_spent', models.DecimalField(verbose_name='Total Spent', max_digits=12, decimal_places=2, default=Decimal('0.00'))),
49
+                ('date_last_order', models.DateTimeField(verbose_name='Last Order Date', blank=True, null=True)),
50 50
                 ('user', models.OneToOneField(verbose_name='User', to=settings.AUTH_USER_MODEL)),
51 51
             ],
52 52
             options={
53
-                'abstract': False,
54 53
                 'verbose_name': 'User record',
55 54
                 'verbose_name_plural': 'User records',
55
+                'abstract': False,
56 56
             },
57 57
             bases=(models.Model,),
58 58
         ),
59 59
         migrations.CreateModel(
60 60
             name='UserSearch',
61 61
             fields=[
62
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
63
-                ('query', models.CharField(max_length=255, verbose_name='Search term', db_index=True)),
64
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
62
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
63
+                ('query', models.CharField(verbose_name='Search term', db_index=True, max_length=255)),
64
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
65 65
                 ('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL)),
66 66
             ],
67 67
             options={
68
-                'abstract': False,
69 68
                 'verbose_name': 'User search query',
70 69
                 'verbose_name_plural': 'User search queries',
70
+                'abstract': False,
71 71
             },
72 72
             bases=(models.Model,),
73 73
         ),

+ 5
- 2
oscar/apps/basket/abstract_models.py Visa fil

@@ -4,6 +4,7 @@ import zlib
4 4
 from django.db import models
5 5
 from django.db.models import Sum
6 6
 from django.conf import settings
7
+from django.utils.encoding import python_2_unicode_compatible
7 8
 from django.utils.timezone import now
8 9
 from django.utils.translation import ugettext_lazy as _
9 10
 from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
@@ -14,6 +15,7 @@ from oscar.core.compat import AUTH_USER_MODEL
14 15
 from oscar.templatetags.currency_filters import currency
15 16
 
16 17
 
18
+@python_2_unicode_compatible
17 19
 class AbstractBasket(models.Model):
18 20
     """
19 21
     Basket object
@@ -74,7 +76,7 @@ class AbstractBasket(models.Model):
74 76
         self._lines = None
75 77
         self.offer_applications = results.OfferApplications()
76 78
 
77
-    def __unicode__(self):
79
+    def __str__(self):
78 80
         return _(
79 81
             u"%(status)s basket (owner: %(owner)s, lines: %(num_lines)d)") \
80 82
             % {'status': self.status,
@@ -538,6 +540,7 @@ class AbstractBasket(models.Model):
538 540
             return 0
539 541
 
540 542
 
543
+@python_2_unicode_compatible
541 544
 class AbstractLine(models.Model):
542 545
     """
543 546
     A line of a basket (product and a quantity)
@@ -590,7 +593,7 @@ class AbstractLine(models.Model):
590 593
         verbose_name = _('Basket line')
591 594
         verbose_name_plural = _('Basket lines')
592 595
 
593
-    def __unicode__(self):
596
+    def __str__(self):
594 597
         return _(
595 598
             u"Basket #%(basket_id)d, Product #%(product_id)d, quantity"
596 599
             u" %(quantity)d") % {'basket_id': self.basket.pk,

+ 6
- 6
oscar/apps/basket/migrations/0001_initial.py Visa fil

@@ -15,17 +15,17 @@ class Migration(migrations.Migration):
15 15
         migrations.CreateModel(
16 16
             name='Basket',
17 17
             fields=[
18
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
19
-                ('status', models.CharField(default=b'Open', max_length=128, verbose_name='Status', choices=[(b'Open', 'Open - currently active'), (b'Merged', 'Merged - superceded by another basket'), (b'Saved', 'Saved - for items to be purchased later'), (b'Frozen', 'Frozen - the basket cannot be modified'), (b'Submitted', 'Submitted - has been ordered at the checkout')])),
20
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
21
-                ('date_merged', models.DateTimeField(null=True, verbose_name='Date merged', blank=True)),
22
-                ('date_submitted', models.DateTimeField(null=True, verbose_name='Date submitted', blank=True)),
18
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
19
+                ('status', models.CharField(verbose_name='Status', choices=[('Open', 'Open - currently active'), ('Merged', 'Merged - superceded by another basket'), ('Saved', 'Saved - for items to be purchased later'), ('Frozen', 'Frozen - the basket cannot be modified'), ('Submitted', 'Submitted - has been ordered at the checkout')], default='Open', max_length=128)),
20
+                ('date_created', models.DateTimeField(verbose_name='Date created', auto_now_add=True)),
21
+                ('date_merged', models.DateTimeField(verbose_name='Date merged', blank=True, null=True)),
22
+                ('date_submitted', models.DateTimeField(verbose_name='Date submitted', blank=True, null=True)),
23 23
                 ('owner', models.ForeignKey(verbose_name='Owner', to=settings.AUTH_USER_MODEL, null=True)),
24 24
             ],
25 25
             options={
26
-                'abstract': False,
27 26
                 'verbose_name': 'Basket',
28 27
                 'verbose_name_plural': 'Baskets',
28
+                'abstract': False,
29 29
             },
30 30
             bases=(models.Model,),
31 31
         ),

oscar/apps/basket/migrations/0002_auto_20140729_1113.py → oscar/apps/basket/migrations/0002_auto_20140805_1510.py Visa fil

@@ -17,46 +17,46 @@ class Migration(migrations.Migration):
17 17
         migrations.AddField(
18 18
             model_name='basket',
19 19
             name='vouchers',
20
-            field=models.ManyToManyField(to='voucher.Voucher', null=True, verbose_name='Vouchers', blank=True),
20
+            field=models.ManyToManyField(verbose_name='Vouchers', blank=True, to='voucher.Voucher', null=True),
21 21
             preserve_default=True,
22 22
         ),
23 23
         migrations.CreateModel(
24 24
             name='Line',
25 25
             fields=[
26
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
27
-                ('line_reference', models.SlugField(max_length=128, verbose_name='Line Reference')),
28
-                ('quantity', models.PositiveIntegerField(default=1, verbose_name='Quantity')),
29
-                ('price_currency', models.CharField(default=b'GBP', max_length=12, verbose_name='Currency')),
30
-                ('price_excl_tax', models.DecimalField(null=True, verbose_name='Price excl. Tax', max_digits=12, decimal_places=2)),
31
-                ('price_incl_tax', models.DecimalField(null=True, verbose_name='Price incl. Tax', max_digits=12, decimal_places=2)),
32
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
26
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
27
+                ('line_reference', models.SlugField(verbose_name='Line Reference', max_length=128)),
28
+                ('quantity', models.PositiveIntegerField(verbose_name='Quantity', default=1)),
29
+                ('price_currency', models.CharField(verbose_name='Currency', default='GBP', max_length=12)),
30
+                ('price_excl_tax', models.DecimalField(verbose_name='Price excl. Tax', max_digits=12, decimal_places=2, null=True)),
31
+                ('price_incl_tax', models.DecimalField(verbose_name='Price incl. Tax', max_digits=12, decimal_places=2, null=True)),
32
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
33 33
                 ('basket', models.ForeignKey(verbose_name='Basket', to='basket.Basket')),
34 34
                 ('product', models.ForeignKey(verbose_name='Product', to='catalogue.Product')),
35 35
                 ('stockrecord', models.ForeignKey(to='partner.StockRecord')),
36 36
             ],
37 37
             options={
38
-                'abstract': False,
39 38
                 'verbose_name': 'Basket line',
40 39
                 'verbose_name_plural': 'Basket lines',
40
+                'abstract': False,
41 41
             },
42 42
             bases=(models.Model,),
43 43
         ),
44 44
         migrations.AlterUniqueTogether(
45 45
             name='line',
46
-            unique_together=set([(b'basket', b'line_reference')]),
46
+            unique_together=set([('basket', 'line_reference')]),
47 47
         ),
48 48
         migrations.CreateModel(
49 49
             name='LineAttribute',
50 50
             fields=[
51
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
52
-                ('value', models.CharField(max_length=255, verbose_name='Value')),
51
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
52
+                ('value', models.CharField(verbose_name='Value', max_length=255)),
53 53
                 ('line', models.ForeignKey(verbose_name='Line', to='basket.Line')),
54 54
                 ('option', models.ForeignKey(verbose_name='Option', to='catalogue.Option')),
55 55
             ],
56 56
             options={
57
-                'abstract': False,
58 57
                 'verbose_name': 'Line attribute',
59 58
                 'verbose_name_plural': 'Line attributes',
59
+                'abstract': False,
60 60
             },
61 61
             bases=(models.Model,),
62 62
         ),

+ 34
- 38
oscar/apps/basket/views.py Visa fil

@@ -1,20 +1,24 @@
1 1
 import json
2
-from six.moves.urllib import parse
3 2
 
3
+from django import shortcuts
4 4
 from django.contrib import messages
5
+from django.shortcuts import redirect
5 6
 from django.template.loader import render_to_string
6 7
 from django.template import RequestContext
7
-from django.core.urlresolvers import reverse, resolve
8
-from django.http import HttpResponseRedirect, Http404, HttpResponse
8
+from django.core.urlresolvers import reverse
9
+from django.http import HttpResponse
9 10
 from django.views.generic import FormView, View
11
+from django.utils.http import is_safe_url
10 12
 from django.utils.translation import ugettext_lazy as _
11 13
 from django.core.exceptions import ObjectDoesNotExist
12
-from django import shortcuts
14
+
13 15
 from extra_views import ModelFormSetView
14 16
 
15 17
 from oscar.core import ajax
18
+from oscar.core.utils import redirect_to_referrer, safe_referrer
16 19
 from oscar.apps.basket import signals
17 20
 from oscar.core.loading import get_class, get_classes, get_model
21
+
18 22
 Applicator = get_class('offer.utils', 'Applicator')
19 23
 (BasketLineFormSet, BasketLineForm, AddToBasketForm, BasketVoucherForm,
20 24
  SavedLineFormSet, SavedLineForm) \
@@ -121,7 +125,7 @@ class BasketView(ModelFormSetView):
121 125
 
122 126
     def get_upsell_messages(self, basket):
123 127
         offers = Applicator().get_offers(self.request, basket)
124
-        applied_offers = basket.offer_applications.offers.values()
128
+        applied_offers = list(basket.offer_applications.offers.values())
125 129
         msgs = []
126 130
         for offer in offers:
127 131
             if offer.is_condition_partially_satisfied(basket) \
@@ -132,9 +136,16 @@ class BasketView(ModelFormSetView):
132 136
                 msgs.append(data)
133 137
         return msgs
134 138
 
139
+    def get_basket_voucher_form(self):
140
+        """
141
+        This is a separate method so that it's easy to e.g. not return a form
142
+        if there are no vouchers available.
143
+        """
144
+        return BasketVoucherForm()
145
+
135 146
     def get_context_data(self, **kwargs):
136 147
         context = super(BasketView, self).get_context_data(**kwargs)
137
-        context['voucher_form'] = BasketVoucherForm()
148
+        context['voucher_form'] = self.get_basket_voucher_form()
138 149
 
139 150
         # Shipping information is included to give an idea of the total order
140 151
         # cost.  It is also important for PayPal Express where the customer
@@ -176,7 +187,7 @@ class BasketView(ModelFormSetView):
176 187
         return context
177 188
 
178 189
     def get_success_url(self):
179
-        return self.request.META.get('HTTP_REFERER', reverse('basket:summary'))
190
+        return safe_referrer(self.request.META, 'basket:summary')
180 191
 
181 192
     def formset_valid(self, formset):
182 193
         # Store offers before any changes are made so we can inform the user of
@@ -206,11 +217,11 @@ class BasketView(ModelFormSetView):
206 217
                     msg = _("You can't save an item for later if you're "
207 218
                             "not logged in!")
208 219
                     flash_messages.error(msg)
209
-                    return HttpResponseRedirect(self.get_success_url())
220
+                    return redirect(self.get_success_url())
210 221
 
211 222
         if save_for_later:
212 223
             # No need to call super if we're moving lines to the saved basket
213
-            response = HttpResponseRedirect(self.get_success_url())
224
+            response = redirect(self.get_success_url())
214 225
         else:
215 226
             # Save changes to basket as per normal
216 227
             response = super(BasketView, self).formset_valid(formset)
@@ -305,8 +316,7 @@ class BasketAddView(FormView):
305 316
         clean_msgs = [m.replace('* ', '') for m in msgs if m.startswith('* ')]
306 317
         messages.error(self.request, ",".join(clean_msgs))
307 318
 
308
-        return HttpResponseRedirect(
309
-            self.request.META.get('HTTP_REFERER', reverse('basket:summary')))
319
+        return redirect_to_referrer(self.request.META, 'basket:summary')
310 320
 
311 321
     def form_valid(self, form):
312 322
         offers_before = self.request.basket.applied_offers()
@@ -335,20 +345,10 @@ class BasketAddView(FormView):
335 345
              'quantity': form.cleaned_data['quantity']})
336 346
 
337 347
     def get_success_url(self):
338
-        url = None
339
-        if self.request.POST.get('next'):
340
-            url = self.request.POST.get('next')
341
-        elif 'HTTP_REFERER' in self.request.META:
342
-            url = self.request.META['HTTP_REFERER']
343
-        if url:
344
-            # We only allow internal URLs so we see if the url resolves
345
-            try:
346
-                resolve(parse.urlparse(url).path)
347
-            except Http404:
348
-                url = None
349
-        if url is None:
350
-            url = reverse('basket:summary')
351
-        return url
348
+        post_url = self.request.POST.get('next')
349
+        if post_url and is_safe_url(post_url):
350
+            return post_url
351
+        return safe_referrer(self.request.META, 'basket:summary')
352 352
 
353 353
 
354 354
 class VoucherAddView(FormView):
@@ -357,7 +357,7 @@ class VoucherAddView(FormView):
357 357
     add_signal = signals.voucher_addition
358 358
 
359 359
     def get(self, request, *args, **kwargs):
360
-        return HttpResponseRedirect(reverse('basket:summary'))
360
+        return redirect('basket:summary')
361 361
 
362 362
     def apply_voucher_to_basket(self, voucher):
363 363
         if not voucher.is_active():
@@ -402,9 +402,7 @@ class VoucherAddView(FormView):
402 402
     def form_valid(self, form):
403 403
         code = form.cleaned_data['code']
404 404
         if not self.request.basket.id:
405
-            return HttpResponseRedirect(
406
-                self.request.META.get('HTTP_REFERER',
407
-                                      reverse('basket:summary')))
405
+            return redirect_to_referrer(self.request.META, 'basket:summary')
408 406
         if self.request.basket.contains_voucher(code):
409 407
             messages.error(
410 408
                 self.request,
@@ -420,12 +418,11 @@ class VoucherAddView(FormView):
420 418
                         'code': code})
421 419
             else:
422 420
                 self.apply_voucher_to_basket(voucher)
423
-        return HttpResponseRedirect(
424
-            self.request.META.get('HTTP_REFERER', reverse('basket:summary')))
421
+        return redirect_to_referrer(self.request.META, 'basket:summary')
425 422
 
426 423
     def form_invalid(self, form):
427 424
         messages.error(self.request, _("Please enter a voucher code"))
428
-        return HttpResponseRedirect(reverse('basket:summary') + '#voucher')
425
+        return redirect(reverse('basket:summary') + '#voucher')
429 426
 
430 427
 
431 428
 class VoucherRemoveView(View):
@@ -434,7 +431,7 @@ class VoucherRemoveView(View):
434 431
     http_method_names = ['post']
435 432
 
436 433
     def post(self, request, *args, **kwargs):
437
-        response = HttpResponseRedirect(reverse('basket:summary'))
434
+        response = redirect('basket:summary')
438 435
 
439 436
         voucher_id = kwargs['pk']
440 437
         if not request.basket.id:
@@ -465,7 +462,7 @@ class SavedView(ModelFormSetView):
465 462
     can_delete = True
466 463
 
467 464
     def get(self, request, *args, **kwargs):
468
-        return HttpResponseRedirect(reverse('basket:summary'))
465
+        return redirect('basket:summary')
469 466
 
470 467
     def get_queryset(self):
471 468
         try:
@@ -477,7 +474,7 @@ class SavedView(ModelFormSetView):
477 474
             return []
478 475
 
479 476
     def get_success_url(self):
480
-        return self.request.META.get('HTTP_REFERER', reverse('basket:summary'))
477
+        return safe_referrer(self.request.META, 'basket:summary')
481 478
 
482 479
     def get_formset_kwargs(self):
483 480
         kwargs = super(SavedView, self).get_formset_kwargs()
@@ -504,7 +501,7 @@ class SavedView(ModelFormSetView):
504 501
             # As we're changing the basket, we need to check if it qualifies
505 502
             # for any new offers.
506 503
             apply_messages(self.request, offers_before)
507
-            response = HttpResponseRedirect(self.get_success_url())
504
+            response = redirect(self.get_success_url())
508 505
         else:
509 506
             response = super(SavedView, self).formset_valid(formset)
510 507
         return response
@@ -515,5 +512,4 @@ class SavedView(ModelFormSetView):
515 512
             '\n'.join(
516 513
                 error for ed in formset.errors for el
517 514
                 in ed.values() for error in el))
518
-        return HttpResponseRedirect(
519
-            self.request.META.get('HTTP_REFERER', reverse('basket:summary')))
515
+        return redirect_to_referrer(self.request.META, 'basket:summary')

+ 46
- 15
oscar/apps/catalogue/abstract_models.py Visa fil

@@ -1,6 +1,5 @@
1
-from django.core.urlresolvers import reverse
2 1
 import os
3
-import six
2
+from django.utils import six
4 3
 from itertools import chain
5 4
 from datetime import datetime, date
6 5
 import logging
@@ -11,17 +10,19 @@ from django.conf import settings
11 10
 from django.contrib.staticfiles.finders import find
12 11
 from django.core.exceptions import ValidationError, ImproperlyConfigured
13 12
 from django.core.files.base import File
13
+from django.core.urlresolvers import reverse
14 14
 from django.core.validators import RegexValidator
15 15
 from django.db import models
16 16
 from django.db.models import Sum, Count
17
+from django.utils.encoding import python_2_unicode_compatible
17 18
 from django.utils.translation import ugettext_lazy as _, pgettext_lazy
18 19
 from django.utils.functional import cached_property
19 20
 from django.contrib.contenttypes.generic import GenericForeignKey
20 21
 from django.contrib.contenttypes.models import ContentType
21 22
 
22 23
 from treebeard.mp_tree import MP_Node
23
-from oscar.core.decorators import deprecated
24 24
 
25
+from oscar.core.decorators import deprecated
25 26
 from oscar.core.utils import slugify
26 27
 from oscar.core.loading import get_classes, get_model
27 28
 from oscar.models.fields import NullCharField, AutoSlugField
@@ -30,6 +31,7 @@ ProductManager, BrowsableProductManager = get_classes(
30 31
     'catalogue.managers', ['ProductManager', 'BrowsableProductManager'])
31 32
 
32 33
 
34
+@python_2_unicode_compatible
33 35
 class AbstractProductClass(models.Model):
34 36
     """
35 37
     Used for defining options and attributes for a subset of products.
@@ -66,7 +68,7 @@ class AbstractProductClass(models.Model):
66 68
         verbose_name = _("Product class")
67 69
         verbose_name_plural = _("Product classes")
68 70
 
69
-    def __unicode__(self):
71
+    def __str__(self):
70 72
         return self.name
71 73
 
72 74
     @property
@@ -74,6 +76,7 @@ class AbstractProductClass(models.Model):
74 76
         return self.attributes.exists()
75 77
 
76 78
 
79
+@python_2_unicode_compatible
77 80
 class AbstractCategory(MP_Node):
78 81
     """
79 82
     A product category. Merely used for navigational purposes; has no
@@ -93,7 +96,7 @@ class AbstractCategory(MP_Node):
93 96
     _slug_separator = '/'
94 97
     _full_name_separator = ' > '
95 98
 
96
-    def __unicode__(self):
99
+    def __str__(self):
97 100
         return self.full_name
98 101
 
99 102
     def update_slug(self, commit=True):
@@ -179,6 +182,7 @@ class AbstractCategory(MP_Node):
179 182
         return self.get_children().count()
180 183
 
181 184
 
185
+@python_2_unicode_compatible
182 186
 class AbstractProductCategory(models.Model):
183 187
     """
184 188
     Joining model between products and categories. Exists to allow customising.
@@ -195,10 +199,11 @@ class AbstractProductCategory(models.Model):
195 199
         verbose_name = _('Product category')
196 200
         verbose_name_plural = _('Product categories')
197 201
 
198
-    def __unicode__(self):
202
+    def __str__(self):
199 203
         return u"<productcategory for product '%s'>" % self.product
200 204
 
201 205
 
206
+@python_2_unicode_compatible
202 207
 class AbstractProduct(models.Model):
203 208
     """
204 209
     The base product object
@@ -308,7 +313,7 @@ class AbstractProduct(models.Model):
308 313
         super(AbstractProduct, self).__init__(*args, **kwargs)
309 314
         self.attr = ProductAttributesContainer(product=self)
310 315
 
311
-    def __unicode__(self):
316
+    def __str__(self):
312 317
         if self.is_child:
313 318
             return u"%s (%s)" % (self.get_title(), self.attribute_summary)
314 319
         return self.get_title()
@@ -559,6 +564,16 @@ class AbstractProduct(models.Model):
559 564
         else:
560 565
             return self.is_discountable
561 566
 
567
+    def get_categories(self):
568
+        """
569
+        Return a product's categories or parent's if there is a parent product.
570
+        """
571
+        if self.is_child:
572
+            return self.parent.categories
573
+        else:
574
+            return self.categories
575
+    get_categories.short_description = _("Categories")
576
+
562 577
     # Images
563 578
 
564 579
     def get_missing_image(self):
@@ -685,7 +700,7 @@ class ProductAttributesContainer(object):
685 700
 
686 701
     def __getattr__(self, name):
687 702
         if not name.startswith('_') and not self.initialised:
688
-            values = list(self.get_values().select_related('attribute'))
703
+            values = self.get_values().select_related('attribute')
689 704
             for v in values:
690 705
                 setattr(self, v.attribute.code, v.value)
691 706
             self.initialised = True
@@ -732,6 +747,7 @@ class ProductAttributesContainer(object):
732 747
                 attribute.save_value(self.product, value)
733 748
 
734 749
 
750
+@python_2_unicode_compatible
735 751
 class AbstractProductAttribute(models.Model):
736 752
     """
737 753
     Defines an attribute for a product class. (For example, number_of_pages for
@@ -796,7 +812,7 @@ class AbstractProductAttribute(models.Model):
796 812
     def is_file(self):
797 813
         return self.type in [self.FILE, self.IMAGE]
798 814
 
799
-    def __unicode__(self):
815
+    def __str__(self):
800 816
         return self.name
801 817
 
802 818
     def save_value(self, product, value):
@@ -887,6 +903,7 @@ class AbstractProductAttribute(models.Model):
887 903
     _validate_image = _validate_file
888 904
 
889 905
 
906
+@python_2_unicode_compatible
890 907
 class AbstractProductAttributeValue(models.Model):
891 908
     """
892 909
     The "through" model for the m2m relationship between catalogue.Product and
@@ -943,7 +960,7 @@ class AbstractProductAttributeValue(models.Model):
943 960
         verbose_name = _('Product attribute value')
944 961
         verbose_name_plural = _('Product attribute values')
945 962
 
946
-    def __unicode__(self):
963
+    def __str__(self):
947 964
         return self.summary()
948 965
 
949 966
     def summary(self):
@@ -973,7 +990,7 @@ class AbstractProductAttributeValue(models.Model):
973 990
         Returns the unicode representation of the related model. You likely
974 991
         want to customise this (and maybe _entity_as_html) if you use entities.
975 992
         """
976
-        return unicode(self.value)
993
+        return six.text_type(self.value)
977 994
 
978 995
     @property
979 996
     def value_as_html(self):
@@ -990,6 +1007,7 @@ class AbstractProductAttributeValue(models.Model):
990 1007
         return mark_safe(self.value)
991 1008
 
992 1009
 
1010
+@python_2_unicode_compatible
993 1011
 class AbstractAttributeOptionGroup(models.Model):
994 1012
     """
995 1013
     Defines a group of options that collectively may be used as an
@@ -999,7 +1017,7 @@ class AbstractAttributeOptionGroup(models.Model):
999 1017
     """
1000 1018
     name = models.CharField(_('Name'), max_length=128)
1001 1019
 
1002
-    def __unicode__(self):
1020
+    def __str__(self):
1003 1021
         return self.name
1004 1022
 
1005 1023
     class Meta:
@@ -1014,6 +1032,7 @@ class AbstractAttributeOptionGroup(models.Model):
1014 1032
         return ", ".join(options)
1015 1033
 
1016 1034
 
1035
+@python_2_unicode_compatible
1017 1036
 class AbstractAttributeOption(models.Model):
1018 1037
     """
1019 1038
     Provides an option within an option group for an attribute type
@@ -1024,7 +1043,7 @@ class AbstractAttributeOption(models.Model):
1024 1043
         verbose_name=_("Group"))
1025 1044
     option = models.CharField(_('Option'), max_length=255)
1026 1045
 
1027
-    def __unicode__(self):
1046
+    def __str__(self):
1028 1047
         return self.option
1029 1048
 
1030 1049
     class Meta:
@@ -1034,6 +1053,7 @@ class AbstractAttributeOption(models.Model):
1034 1053
         verbose_name_plural = _('Attribute options')
1035 1054
 
1036 1055
 
1056
+@python_2_unicode_compatible
1037 1057
 class AbstractOption(models.Model):
1038 1058
     """
1039 1059
     An option that can be selected for a particular item when the product
@@ -1064,7 +1084,7 @@ class AbstractOption(models.Model):
1064 1084
         verbose_name = _("Option")
1065 1085
         verbose_name_plural = _("Options")
1066 1086
 
1067
-    def __unicode__(self):
1087
+    def __str__(self):
1068 1088
         return self.name
1069 1089
 
1070 1090
     @property
@@ -1110,6 +1130,7 @@ class MissingProductImage(object):
1110 1130
                                            settings.MEDIA_ROOT))
1111 1131
 
1112 1132
 
1133
+@python_2_unicode_compatible
1113 1134
 class AbstractProductImage(models.Model):
1114 1135
     """
1115 1136
     An image of a product
@@ -1137,7 +1158,7 @@ class AbstractProductImage(models.Model):
1137 1158
         verbose_name = _('Product image')
1138 1159
         verbose_name_plural = _('Product images')
1139 1160
 
1140
-    def __unicode__(self):
1161
+    def __str__(self):
1141 1162
         return u"Image of '%s'" % self.product
1142 1163
 
1143 1164
     def is_primary(self):
@@ -1145,3 +1166,13 @@ class AbstractProductImage(models.Model):
1145 1166
         Return bool if image display order is 0
1146 1167
         """
1147 1168
         return self.display_order == 0
1169
+
1170
+    def delete(self, *args, **kwargs):
1171
+        """
1172
+        Always keep the display_order as consecutive integers. This avoids
1173
+        issue #855.
1174
+        """
1175
+        super(AbstractProductImage, self).delete(*args, **kwargs)
1176
+        for idx, image in enumerate(self.product.images.all()):
1177
+            image.display_order = idx
1178
+            image.save()

+ 83
- 83
oscar/apps/catalogue/migrations/0001_initial.py Visa fil

@@ -2,42 +2,42 @@
2 2
 from __future__ import unicode_literals
3 3
 
4 4
 from django.db import models, migrations
5
-import oscar.models.fields.autoslugfield
6 5
 import oscar.models.fields
7
-import django.db.models.deletion
8 6
 import django.core.validators
7
+import django.db.models.deletion
8
+import oscar.models.fields.autoslugfield
9 9
 
10 10
 
11 11
 class Migration(migrations.Migration):
12 12
 
13 13
     dependencies = [
14
-        ('contenttypes', '__latest__'),
14
+        ('contenttypes', '0001_initial'),
15 15
     ]
16 16
 
17 17
     operations = [
18 18
         migrations.CreateModel(
19 19
             name='AttributeOption',
20 20
             fields=[
21
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
22
-                ('option', models.CharField(max_length=255, verbose_name='Option')),
21
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
22
+                ('option', models.CharField(verbose_name='Option', max_length=255)),
23 23
             ],
24 24
             options={
25
-                'abstract': False,
26 25
                 'verbose_name': 'Attribute option',
27 26
                 'verbose_name_plural': 'Attribute options',
27
+                'abstract': False,
28 28
             },
29 29
             bases=(models.Model,),
30 30
         ),
31 31
         migrations.CreateModel(
32 32
             name='AttributeOptionGroup',
33 33
             fields=[
34
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
35
-                ('name', models.CharField(max_length=128, verbose_name='Name')),
34
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
35
+                ('name', models.CharField(verbose_name='Name', max_length=128)),
36 36
             ],
37 37
             options={
38
-                'abstract': False,
39 38
                 'verbose_name': 'Attribute option group',
40 39
                 'verbose_name_plural': 'Attribute option groups',
40
+                'abstract': False,
41 41
             },
42 42
             bases=(models.Model,),
43 43
         ),
@@ -50,108 +50,108 @@ class Migration(migrations.Migration):
50 50
         migrations.CreateModel(
51 51
             name='Category',
52 52
             fields=[
53
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
53
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
54 54
                 ('path', models.CharField(unique=True, max_length=255)),
55 55
                 ('depth', models.PositiveIntegerField()),
56 56
                 ('numchild', models.PositiveIntegerField(default=0)),
57
-                ('name', models.CharField(max_length=255, verbose_name='Name', db_index=True)),
57
+                ('name', models.CharField(verbose_name='Name', db_index=True, max_length=255)),
58 58
                 ('description', models.TextField(verbose_name='Description', blank=True)),
59
-                ('image', models.ImageField(max_length=255, upload_to=b'categories', null=True, verbose_name='Image', blank=True)),
60
-                ('slug', models.SlugField(verbose_name='Slug', max_length=255, editable=False)),
61
-                ('full_name', models.CharField(verbose_name='Full Name', max_length=255, editable=False, db_index=True)),
59
+                ('image', models.ImageField(verbose_name='Image', upload_to='categories', blank=True, null=True, max_length=255)),
60
+                ('slug', models.SlugField(editable=False, verbose_name='Slug', max_length=255)),
61
+                ('full_name', models.CharField(editable=False, verbose_name='Full Name', db_index=True, max_length=255)),
62 62
             ],
63 63
             options={
64
-                'ordering': [b'full_name'],
65
-                'abstract': False,
66 64
                 'verbose_name': 'Category',
67 65
                 'verbose_name_plural': 'Categories',
66
+                'ordering': ['full_name'],
67
+                'abstract': False,
68 68
             },
69 69
             bases=(models.Model,),
70 70
         ),
71 71
         migrations.CreateModel(
72 72
             name='Option',
73 73
             fields=[
74
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
75
-                ('name', models.CharField(max_length=128, verbose_name='Name')),
76
-                ('code', oscar.models.fields.autoslugfield.AutoSlugField(populate_from=b'name', editable=False, max_length=128, blank=True, unique=True, verbose_name='Code')),
77
-                ('type', models.CharField(default=b'Required', max_length=128, verbose_name='Status', choices=[(b'Required', 'Required - a value for this option must be specified'), (b'Optional', 'Optional - a value for this option can be omitted')])),
74
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
75
+                ('name', models.CharField(verbose_name='Name', max_length=128)),
76
+                ('code', oscar.models.fields.autoslugfield.AutoSlugField(editable=False, verbose_name='Code', blank=True, max_length=128, populate_from='name', unique=True)),
77
+                ('type', models.CharField(verbose_name='Status', choices=[('Required', 'Required - a value for this option must be specified'), ('Optional', 'Optional - a value for this option can be omitted')], default='Required', max_length=128)),
78 78
             ],
79 79
             options={
80
-                'abstract': False,
81 80
                 'verbose_name': 'Option',
82 81
                 'verbose_name_plural': 'Options',
82
+                'abstract': False,
83 83
             },
84 84
             bases=(models.Model,),
85 85
         ),
86 86
         migrations.CreateModel(
87 87
             name='Product',
88 88
             fields=[
89
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
90
-                ('structure', models.CharField(default=b'standalone', max_length=10, verbose_name='Product structure', choices=[(b'standalone', 'Stand-alone product'), (b'parent', 'Parent product'), (b'child', 'Child product')])),
91
-                ('upc', oscar.models.fields.NullCharField(max_length=64, help_text='Universal Product Code (UPC) is an identifier for a product which is not specific to a particular  supplier. Eg an ISBN for a book.', unique=True, verbose_name='UPC')),
92
-                ('title', models.CharField(max_length=255, verbose_name='Title', blank=True)),
93
-                ('slug', models.SlugField(max_length=255, verbose_name='Slug')),
89
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
90
+                ('structure', models.CharField(verbose_name='Product structure', choices=[('standalone', 'Stand-alone product'), ('parent', 'Parent product'), ('child', 'Child product')], default='standalone', max_length=10)),
91
+                ('upc', oscar.models.fields.NullCharField(verbose_name='UPC', max_length=64, help_text='Universal Product Code (UPC) is an identifier for a product which is not specific to a particular  supplier. Eg an ISBN for a book.', unique=True)),
92
+                ('title', models.CharField(verbose_name='Title', blank=True, max_length=255)),
93
+                ('slug', models.SlugField(verbose_name='Slug', max_length=255)),
94 94
                 ('description', models.TextField(verbose_name='Description', blank=True)),
95
-                ('rating', models.FloatField(verbose_name='Rating', null=True, editable=False)),
96
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
97
-                ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated', db_index=True)),
98
-                ('is_discountable', models.BooleanField(default=True, help_text='This flag indicates if this product can be used in an offer or not', verbose_name='Is discountable?')),
99
-                ('parent', models.ForeignKey(blank=True, to='catalogue.Product', help_text="Only choose a parent product if you're creating a child product.  For example if this is a size 4 of a particular t-shirt.  Leave blank if this is a stand-alone product (i.e. there is only one version of this product).", null=True, verbose_name='Parent product')),
100
-                ('product_options', models.ManyToManyField(to='catalogue.Option', verbose_name='Product Options', blank=True)),
95
+                ('rating', models.FloatField(editable=False, verbose_name='Rating', null=True)),
96
+                ('date_created', models.DateTimeField(verbose_name='Date created', auto_now_add=True)),
97
+                ('date_updated', models.DateTimeField(verbose_name='Date updated', auto_now=True, db_index=True)),
98
+                ('is_discountable', models.BooleanField(verbose_name='Is discountable?', help_text='This flag indicates if this product can be used in an offer or not', default=True)),
99
+                ('parent', models.ForeignKey(verbose_name='Parent product', blank=True, help_text="Only choose a parent product if you're creating a child product.  For example if this is a size 4 of a particular t-shirt.  Leave blank if this is a stand-alone product (i.e. there is only one version of this product).", to='catalogue.Product', null=True)),
100
+                ('product_options', models.ManyToManyField(verbose_name='Product Options', blank=True, to='catalogue.Option')),
101 101
             ],
102 102
             options={
103
-                'ordering': [b'-date_created'],
104
-                'abstract': False,
105 103
                 'verbose_name': 'Product',
106 104
                 'verbose_name_plural': 'Products',
105
+                'ordering': ['-date_created'],
106
+                'abstract': False,
107 107
             },
108 108
             bases=(models.Model,),
109 109
         ),
110 110
         migrations.CreateModel(
111 111
             name='ProductAttribute',
112 112
             fields=[
113
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
114
-                ('name', models.CharField(max_length=128, verbose_name='Name')),
115
-                ('code', models.SlugField(max_length=128, verbose_name='Code', validators=[django.core.validators.RegexValidator(regex=b'^[a-zA-Z\\-_][0-9a-zA-Z\\-_]*$', message="Code can only contain the letters a-z, A-Z, digits, minus and underscores, and can't start with a digit")])),
116
-                ('type', models.CharField(default=b'text', max_length=20, verbose_name='Type', choices=[(b'text', 'Text'), (b'integer', 'Integer'), (b'boolean', 'True / False'), (b'float', 'Float'), (b'richtext', 'Rich Text'), (b'date', 'Date'), (b'option', 'Option'), (b'entity', 'Entity'), (b'file', 'File'), (b'image', 'Image')])),
117
-                ('required', models.BooleanField(default=False, verbose_name='Required')),
118
-                ('option_group', models.ForeignKey(blank=True, to='catalogue.AttributeOptionGroup', help_text='Select an option group if using type "Option"', null=True, verbose_name='Option Group')),
113
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
114
+                ('name', models.CharField(verbose_name='Name', max_length=128)),
115
+                ('code', models.SlugField(verbose_name='Code', validators=[django.core.validators.RegexValidator(regex='^[a-zA-Z\\-_][0-9a-zA-Z\\-_]*$', message="Code can only contain the letters a-z, A-Z, digits, minus and underscores, and can't start with a digit")], max_length=128)),
116
+                ('type', models.CharField(verbose_name='Type', choices=[('text', 'Text'), ('integer', 'Integer'), ('boolean', 'True / False'), ('float', 'Float'), ('richtext', 'Rich Text'), ('date', 'Date'), ('option', 'Option'), ('entity', 'Entity'), ('file', 'File'), ('image', 'Image')], default='text', max_length=20)),
117
+                ('required', models.BooleanField(verbose_name='Required', default=False)),
118
+                ('option_group', models.ForeignKey(verbose_name='Option Group', blank=True, help_text='Select an option group if using type "Option"', to='catalogue.AttributeOptionGroup', null=True)),
119 119
             ],
120 120
             options={
121
-                'ordering': [b'code'],
122
-                'abstract': False,
123 121
                 'verbose_name': 'Product attribute',
124 122
                 'verbose_name_plural': 'Product attributes',
123
+                'ordering': ['code'],
124
+                'abstract': False,
125 125
             },
126 126
             bases=(models.Model,),
127 127
         ),
128 128
         migrations.CreateModel(
129 129
             name='ProductAttributeValue',
130 130
             fields=[
131
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
132
-                ('value_text', models.TextField(null=True, verbose_name='Text', blank=True)),
133
-                ('value_integer', models.IntegerField(null=True, verbose_name='Integer', blank=True)),
131
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
132
+                ('value_text', models.TextField(verbose_name='Text', blank=True, null=True)),
133
+                ('value_integer', models.IntegerField(verbose_name='Integer', blank=True, null=True)),
134 134
                 ('value_boolean', models.NullBooleanField(verbose_name='Boolean')),
135
-                ('value_float', models.FloatField(null=True, verbose_name='Float', blank=True)),
136
-                ('value_richtext', models.TextField(null=True, verbose_name='Richtext', blank=True)),
137
-                ('value_date', models.DateField(null=True, verbose_name='Date', blank=True)),
138
-                ('value_file', models.FileField(max_length=255, null=True, upload_to=b'images/products/%Y/%m/', blank=True)),
139
-                ('value_image', models.ImageField(max_length=255, null=True, upload_to=b'images/products/%Y/%m/', blank=True)),
140
-                ('entity_object_id', models.PositiveIntegerField(null=True, editable=False, blank=True)),
135
+                ('value_float', models.FloatField(verbose_name='Float', blank=True, null=True)),
136
+                ('value_richtext', models.TextField(verbose_name='Richtext', blank=True, null=True)),
137
+                ('value_date', models.DateField(verbose_name='Date', blank=True, null=True)),
138
+                ('value_file', models.FileField(null=True, upload_to='images/products/%Y/%m/', blank=True, max_length=255)),
139
+                ('value_image', models.ImageField(null=True, upload_to='images/products/%Y/%m/', blank=True, max_length=255)),
140
+                ('entity_object_id', models.PositiveIntegerField(editable=False, blank=True, null=True)),
141 141
                 ('attribute', models.ForeignKey(verbose_name='Attribute', to='catalogue.ProductAttribute')),
142
-                ('entity_content_type', models.ForeignKey(blank=True, editable=False, to='contenttypes.ContentType', null=True)),
142
+                ('entity_content_type', models.ForeignKey(editable=False, blank=True, to='contenttypes.ContentType', null=True)),
143 143
             ],
144 144
             options={
145
-                'abstract': False,
146 145
                 'verbose_name': 'Product attribute value',
147 146
                 'verbose_name_plural': 'Product attribute values',
147
+                'abstract': False,
148 148
             },
149 149
             bases=(models.Model,),
150 150
         ),
151 151
         migrations.AddField(
152 152
             model_name='product',
153 153
             name='attributes',
154
-            field=models.ManyToManyField(to='catalogue.ProductAttribute', verbose_name='Attributes', through='catalogue.ProductAttributeValue'),
154
+            field=models.ManyToManyField(verbose_name='Attributes', through='catalogue.ProductAttributeValue', to='catalogue.ProductAttribute'),
155 155
             preserve_default=True,
156 156
         ),
157 157
         migrations.AddField(
@@ -168,26 +168,26 @@ class Migration(migrations.Migration):
168 168
         ),
169 169
         migrations.AlterUniqueTogether(
170 170
             name='productattributevalue',
171
-            unique_together=set([(b'attribute', b'product')]),
171
+            unique_together=set([('attribute', 'product')]),
172 172
         ),
173 173
         migrations.CreateModel(
174 174
             name='ProductCategory',
175 175
             fields=[
176
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
176
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
177 177
                 ('category', models.ForeignKey(verbose_name='Category', to='catalogue.Category')),
178 178
             ],
179 179
             options={
180
-                'ordering': [b'product', b'category'],
181
-                'abstract': False,
182 180
                 'verbose_name': 'Product category',
183 181
                 'verbose_name_plural': 'Product categories',
182
+                'ordering': ['product', 'category'],
183
+                'abstract': False,
184 184
             },
185 185
             bases=(models.Model,),
186 186
         ),
187 187
         migrations.AddField(
188 188
             model_name='product',
189 189
             name='categories',
190
-            field=models.ManyToManyField(to='catalogue.Category', verbose_name='Categories', through='catalogue.ProductCategory'),
190
+            field=models.ManyToManyField(verbose_name='Categories', through='catalogue.ProductCategory', to='catalogue.Category'),
191 191
             preserve_default=True,
192 192
         ),
193 193
         migrations.AddField(
@@ -198,23 +198,23 @@ class Migration(migrations.Migration):
198 198
         ),
199 199
         migrations.AlterUniqueTogether(
200 200
             name='productcategory',
201
-            unique_together=set([(b'product', b'category')]),
201
+            unique_together=set([('product', 'category')]),
202 202
         ),
203 203
         migrations.CreateModel(
204 204
             name='ProductClass',
205 205
             fields=[
206
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
207
-                ('name', models.CharField(max_length=128, verbose_name='Name')),
208
-                ('slug', oscar.models.fields.autoslugfield.AutoSlugField(populate_from=b'name', editable=False, max_length=128, blank=True, unique=True, verbose_name='Slug')),
209
-                ('requires_shipping', models.BooleanField(default=True, verbose_name='Requires shipping?')),
210
-                ('track_stock', models.BooleanField(default=True, verbose_name='Track stock levels?')),
211
-                ('options', models.ManyToManyField(to='catalogue.Option', verbose_name='Options', blank=True)),
206
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
207
+                ('name', models.CharField(verbose_name='Name', max_length=128)),
208
+                ('slug', oscar.models.fields.autoslugfield.AutoSlugField(editable=False, verbose_name='Slug', blank=True, max_length=128, populate_from='name', unique=True)),
209
+                ('requires_shipping', models.BooleanField(verbose_name='Requires shipping?', default=True)),
210
+                ('track_stock', models.BooleanField(verbose_name='Track stock levels?', default=True)),
211
+                ('options', models.ManyToManyField(verbose_name='Options', blank=True, to='catalogue.Option')),
212 212
             ],
213 213
             options={
214
-                'ordering': [b'name'],
215
-                'abstract': False,
216 214
                 'verbose_name': 'Product class',
217 215
                 'verbose_name_plural': 'Product classes',
216
+                'ordering': ['name'],
217
+                'abstract': False,
218 218
             },
219 219
             bases=(models.Model,),
220 220
         ),
@@ -227,49 +227,49 @@ class Migration(migrations.Migration):
227 227
         migrations.AddField(
228 228
             model_name='product',
229 229
             name='product_class',
230
-            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, verbose_name='Product Type', to='catalogue.ProductClass', help_text='Choose what type of product this is', null=True),
230
+            field=models.ForeignKey(verbose_name='Product Type', on_delete=django.db.models.deletion.PROTECT, help_text='Choose what type of product this is', to='catalogue.ProductClass', null=True),
231 231
             preserve_default=True,
232 232
         ),
233 233
         migrations.CreateModel(
234 234
             name='ProductImage',
235 235
             fields=[
236
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
237
-                ('original', models.ImageField(upload_to=b'images/products/%Y/%m/', max_length=255, verbose_name='Original')),
238
-                ('caption', models.CharField(max_length=200, verbose_name='Caption', blank=True)),
239
-                ('display_order', models.PositiveIntegerField(default=0, help_text='An image with a display order of zero will be the primary image for a product', verbose_name='Display Order')),
240
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
236
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
237
+                ('original', models.ImageField(verbose_name='Original', upload_to='images/products/%Y/%m/', max_length=255)),
238
+                ('caption', models.CharField(verbose_name='Caption', blank=True, max_length=200)),
239
+                ('display_order', models.PositiveIntegerField(verbose_name='Display Order', help_text='An image with a display order of zero will be the primary image for a product', default=0)),
240
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
241 241
                 ('product', models.ForeignKey(verbose_name='Product', to='catalogue.Product')),
242 242
             ],
243 243
             options={
244
-                'ordering': [b'display_order'],
245
-                'abstract': False,
246 244
                 'verbose_name': 'Product image',
247 245
                 'verbose_name_plural': 'Product images',
246
+                'ordering': ['display_order'],
247
+                'abstract': False,
248 248
             },
249 249
             bases=(models.Model,),
250 250
         ),
251 251
         migrations.AlterUniqueTogether(
252 252
             name='productimage',
253
-            unique_together=set([(b'product', b'display_order')]),
253
+            unique_together=set([('product', 'display_order')]),
254 254
         ),
255 255
         migrations.CreateModel(
256 256
             name='ProductRecommendation',
257 257
             fields=[
258
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
259
-                ('ranking', models.PositiveSmallIntegerField(default=0, help_text='Determines order of the products. A product with a higher value will appear before one with a lower ranking.', verbose_name='Ranking')),
258
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
259
+                ('ranking', models.PositiveSmallIntegerField(verbose_name='Ranking', help_text='Determines order of the products. A product with a higher value will appear before one with a lower ranking.', default=0)),
260 260
             ],
261 261
             options={
262
-                'ordering': [b'primary', b'-ranking'],
263
-                'abstract': False,
264 262
                 'verbose_name': 'Product recommendation',
265 263
                 'verbose_name_plural': 'Product recomendations',
264
+                'ordering': ['primary', '-ranking'],
265
+                'abstract': False,
266 266
             },
267 267
             bases=(models.Model,),
268 268
         ),
269 269
         migrations.AddField(
270 270
             model_name='product',
271 271
             name='recommended_products',
272
-            field=models.ManyToManyField(to='catalogue.Product', verbose_name='Recommended Products', through='catalogue.ProductRecommendation', blank=True),
272
+            field=models.ManyToManyField(verbose_name='Recommended Products', through='catalogue.ProductRecommendation', blank=True, to='catalogue.Product'),
273 273
             preserve_default=True,
274 274
         ),
275 275
         migrations.AddField(
@@ -286,6 +286,6 @@ class Migration(migrations.Migration):
286 286
         ),
287 287
         migrations.AlterUniqueTogether(
288 288
             name='productrecommendation',
289
-            unique_together=set([(b'primary', b'recommendation')]),
289
+            unique_together=set([('primary', 'recommendation')]),
290 290
         ),
291 291
     ]

+ 6
- 3
oscar/apps/catalogue/reviews/abstract_models.py Visa fil

@@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError
3 3
 from django.core.urlresolvers import reverse
4 4
 from django.db import models
5 5
 from django.db.models import Sum, Count
6
+from django.utils.encoding import python_2_unicode_compatible
6 7
 from django.utils.translation import ugettext_lazy as _, pgettext_lazy
7 8
 
8 9
 from oscar.apps.catalogue.reviews.managers import ApprovedReviewsManager
@@ -10,6 +11,7 @@ from oscar.core.compat import AUTH_USER_MODEL
10 11
 from oscar.core import validators
11 12
 
12 13
 
14
+@python_2_unicode_compatible
13 15
 class AbstractProductReview(models.Model):
14 16
     """
15 17
     A review of a product
@@ -43,7 +45,7 @@ class AbstractProductReview(models.Model):
43 45
     email = models.EmailField(_("Email"), blank=True)
44 46
     homepage = models.URLField(_("URL"), blank=True)
45 47
 
46
-    FOR_MODERATION, APPROVED, REJECTED = list(range(0, 3))
48
+    FOR_MODERATION, APPROVED, REJECTED = 0, 1, 2
47 49
     STATUS_CHOICES = (
48 50
         (FOR_MODERATION, _("Requires moderation")),
49 51
         (APPROVED, _("Approved")),
@@ -83,7 +85,7 @@ class AbstractProductReview(models.Model):
83 85
         }
84 86
         return reverse('catalogue:reviews-detail', kwargs=kwargs)
85 87
 
86
-    def __unicode__(self):
88
+    def __str__(self):
87 89
         return self.title
88 90
 
89 91
     def clean(self):
@@ -175,6 +177,7 @@ class AbstractProductReview(models.Model):
175 177
         return True, ""
176 178
 
177 179
 
180
+@python_2_unicode_compatible
178 181
 class AbstractVote(models.Model):
179 182
     """
180 183
     Records user ratings as yes/no vote.
@@ -200,7 +203,7 @@ class AbstractVote(models.Model):
200 203
         verbose_name = _('Vote')
201 204
         verbose_name_plural = _('Votes')
202 205
 
203
-    def __unicode__(self):
206
+    def __str__(self):
204 207
         return u"%s vote for %s" % (self.delta, self.review)
205 208
 
206 209
     def clean(self):

+ 15
- 15
oscar/apps/catalogue/reviews/migrations/0001_initial.py Visa fil

@@ -18,51 +18,51 @@ class Migration(migrations.Migration):
18 18
         migrations.CreateModel(
19 19
             name='ProductReview',
20 20
             fields=[
21
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
21
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
22 22
                 ('score', models.SmallIntegerField(verbose_name='Score', choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)])),
23
-                ('title', models.CharField(max_length=255, verbose_name='Title', validators=[oscar.core.validators.non_whitespace])),
23
+                ('title', models.CharField(verbose_name='Title', validators=[oscar.core.validators.non_whitespace], max_length=255)),
24 24
                 ('body', models.TextField(verbose_name='Body')),
25
-                ('name', models.CharField(max_length=255, verbose_name='Name', blank=True)),
26
-                ('email', models.EmailField(max_length=75, verbose_name='Email', blank=True)),
25
+                ('name', models.CharField(verbose_name='Name', blank=True, max_length=255)),
26
+                ('email', models.EmailField(verbose_name='Email', blank=True, max_length=75)),
27 27
                 ('homepage', models.URLField(verbose_name='URL', blank=True)),
28
-                ('status', models.SmallIntegerField(default=1, verbose_name='Status', choices=[(0, 'Requires moderation'), (1, 'Approved'), (2, 'Rejected')])),
29
-                ('total_votes', models.IntegerField(default=0, verbose_name='Total Votes')),
30
-                ('delta_votes', models.IntegerField(default=0, verbose_name='Delta Votes', db_index=True)),
28
+                ('status', models.SmallIntegerField(verbose_name='Status', choices=[(0, 'Requires moderation'), (1, 'Approved'), (2, 'Rejected')], default=1)),
29
+                ('total_votes', models.IntegerField(verbose_name='Total Votes', default=0)),
30
+                ('delta_votes', models.IntegerField(verbose_name='Delta Votes', db_index=True, default=0)),
31 31
                 ('date_created', models.DateTimeField(auto_now_add=True)),
32
-                ('product', models.ForeignKey(to='catalogue.Product', on_delete=django.db.models.deletion.SET_NULL, null=True)),
32
+                ('product', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, to='catalogue.Product', null=True)),
33 33
                 ('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True)),
34 34
             ],
35 35
             options={
36
-                'ordering': [b'-delta_votes', b'id'],
37
-                'abstract': False,
38 36
                 'verbose_name': 'Product review',
39 37
                 'verbose_name_plural': 'Product reviews',
38
+                'ordering': ['-delta_votes', 'id'],
39
+                'abstract': False,
40 40
             },
41 41
             bases=(models.Model,),
42 42
         ),
43 43
         migrations.AlterUniqueTogether(
44 44
             name='productreview',
45
-            unique_together=set([(b'product', b'user')]),
45
+            unique_together=set([('product', 'user')]),
46 46
         ),
47 47
         migrations.CreateModel(
48 48
             name='Vote',
49 49
             fields=[
50
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
50
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
51 51
                 ('delta', models.SmallIntegerField(verbose_name='Delta', choices=[(1, 'Up'), (-1, 'Down')])),
52 52
                 ('date_created', models.DateTimeField(auto_now_add=True)),
53 53
                 ('review', models.ForeignKey(to='reviews.ProductReview')),
54 54
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
55 55
             ],
56 56
             options={
57
-                'ordering': [b'-date_created'],
58
-                'abstract': False,
59 57
                 'verbose_name': 'Vote',
60 58
                 'verbose_name_plural': 'Votes',
59
+                'ordering': ['-date_created'],
60
+                'abstract': False,
61 61
             },
62 62
             bases=(models.Model,),
63 63
         ),
64 64
         migrations.AlterUniqueTogether(
65 65
             name='vote',
66
-            unique_together=set([(b'user', b'review')]),
66
+            unique_together=set([('user', 'review')]),
67 67
         ),
68 68
     ]

+ 4
- 5
oscar/apps/catalogue/reviews/views.py Visa fil

@@ -1,11 +1,11 @@
1
-from django.http import HttpResponseRedirect
2
-from django.shortcuts import get_object_or_404
1
+from django.shortcuts import get_object_or_404, redirect
3 2
 from django.views.generic import ListView, DetailView, CreateView, View
4 3
 from django.contrib import messages
5 4
 from oscar.core.loading import get_model
6 5
 from django.utils.translation import ugettext_lazy as _
7 6
 
8 7
 from oscar.core.loading import get_classes
8
+from oscar.core.utils import redirect_to_referrer
9 9
 from oscar.apps.catalogue.reviews.signals import review_added
10 10
 
11 11
 ProductReviewForm, VoteForm = get_classes(
@@ -32,7 +32,7 @@ class CreateProductReview(CreateView):
32 32
             else:
33 33
                 message = _("You can't leave a review for this product.")
34 34
             messages.warning(self.request, message)
35
-            return HttpResponseRedirect(self.product.get_absolute_url())
35
+            return redirect(self.product.get_absolute_url())
36 36
 
37 37
         return super(CreateProductReview, self).dispatch(
38 38
             request, *args, **kwargs)
@@ -98,8 +98,7 @@ class AddVoteView(View):
98 98
             for error_list in form.errors.values():
99 99
                 for msg in error_list:
100 100
                     messages.error(request, msg)
101
-        return HttpResponseRedirect(
102
-            request.META.get('HTTP_REFERER', product.get_absolute_url()))
101
+        return redirect_to_referrer(request.META, product.get_absolute_url())
103 102
 
104 103
 
105 104
 class ProductReviewList(ListView):

+ 26
- 26
oscar/apps/checkout/mixins.py Visa fil

@@ -11,7 +11,6 @@ OrderCreator = get_class('order.utils', 'OrderCreator')
11 11
 Dispatcher = get_class('customer.utils', 'Dispatcher')
12 12
 CheckoutSessionMixin = get_class('checkout.session', 'CheckoutSessionMixin')
13 13
 ShippingAddress = get_model('order', 'ShippingAddress')
14
-CommunicationEvent = get_model('order', 'CommunicationEvent')
15 14
 OrderNumberGenerator = get_class('order.utils', 'OrderNumberGenerator')
16 15
 PaymentEventType = get_model('order', 'PaymentEventType')
17 16
 PaymentEvent = get_model('order', 'PaymentEvent')
@@ -235,7 +234,7 @@ class OrderPlacementMixin(CheckoutSessionMixin):
235 234
         order is submitted.
236 235
         """
237 236
         # Send confirmation message (normally an email)
238
-        self.send_confirmation_message(order)
237
+        self.send_confirmation_message(order, self.communication_type_code)
239 238
 
240 239
         # Flush all session data
241 240
         self.checkout_session.flush()
@@ -255,27 +254,8 @@ class OrderPlacementMixin(CheckoutSessionMixin):
255 254
     def get_success_url(self):
256 255
         return reverse('checkout:thank-you')
257 256
 
258
-    def send_confirmation_message(self, order, **kwargs):
259
-        code = self.communication_type_code
260
-        ctx = {'user': self.request.user,
261
-               'order': order,
262
-               'site': get_current_site(self.request),
263
-               'lines': order.lines.all()}
264
-
265
-        if not self.request.user.is_authenticated():
266
-            # Attempt to add the anon order status URL to the email template
267
-            # ctx.
268
-            try:
269
-                path = reverse('customer:anon-order',
270
-                               kwargs={'order_number': order.number,
271
-                                       'hash': order.verification_hash()})
272
-            except NoReverseMatch:
273
-                # We don't care that much if we can't resolve the URL
274
-                pass
275
-            else:
276
-                site = Site.objects.get_current()
277
-                ctx['status_url'] = 'http://%s%s' % (site.domain, path)
278
-
257
+    def send_confirmation_message(self, order, code, **kwargs):
258
+        ctx = self.get_message_context(order)
279 259
         try:
280 260
             event_type = CommunicationEventType.objects.get(code=code)
281 261
         except CommunicationEventType.DoesNotExist:
@@ -286,9 +266,6 @@ class OrderPlacementMixin(CheckoutSessionMixin):
286 266
             messages = CommunicationEventType.objects.get_and_render(code, ctx)
287 267
             event_type = None
288 268
         else:
289
-            # Create CommunicationEvent
290
-            CommunicationEvent._default_manager.create(
291
-                order=order, event_type=event_type)
292 269
             messages = event_type.get_messages(ctx)
293 270
 
294 271
         if messages and messages['body']:
@@ -300,6 +277,29 @@ class OrderPlacementMixin(CheckoutSessionMixin):
300 277
             logger.warning("Order #%s - no %s communication event type",
301 278
                            order.number, code)
302 279
 
280
+    def get_message_context(self, order):
281
+        ctx = {
282
+            'user': self.request.user,
283
+            'order': order,
284
+            'site': get_current_site(self.request),
285
+            'lines': order.lines.all()
286
+        }
287
+
288
+        if not self.request.user.is_authenticated():
289
+            # Attempt to add the anon order status URL to the email template
290
+            # ctx.
291
+            try:
292
+                path = reverse('customer:anon-order',
293
+                               kwargs={'order_number': order.number,
294
+                                       'hash': order.verification_hash()})
295
+            except NoReverseMatch:
296
+                # We don't care that much if we can't resolve the URL
297
+                pass
298
+            else:
299
+                site = Site.objects.get_current()
300
+                ctx['status_url'] = 'http://%s%s' % (site.domain, path)
301
+        return ctx
302
+
303 303
     # Basket helpers
304 304
     # --------------
305 305
 

+ 1
- 1
oscar/apps/checkout/views.py Visa fil

@@ -1,4 +1,4 @@
1
-import six
1
+from django.utils import six
2 2
 import logging
3 3
 
4 4
 from django import http

+ 20
- 9
oscar/apps/customer/abstract_models.py Visa fil

@@ -1,4 +1,4 @@
1
-import six
1
+from django.utils import six
2 2
 import hashlib
3 3
 import random
4 4
 
@@ -9,6 +9,7 @@ from django.db import models
9 9
 from django.template import Template, Context, TemplateDoesNotExist
10 10
 from django.template.loader import get_template
11 11
 from django.utils import timezone
12
+from django.utils.encoding import python_2_unicode_compatible
12 13
 from django.utils.translation import ugettext_lazy as _
13 14
 
14 15
 from oscar.apps.customer.managers import CommunicationTypeManager
@@ -104,6 +105,7 @@ class AbstractUser(auth_models.AbstractBaseUser,
104 105
         self._migrate_alerts_to_user()
105 106
 
106 107
 
108
+@python_2_unicode_compatible
107 109
 class AbstractEmail(models.Model):
108 110
     """
109 111
     This is a record of all emails sent to a customer.
@@ -122,11 +124,12 @@ class AbstractEmail(models.Model):
122 124
         verbose_name = _('Email')
123 125
         verbose_name_plural = _('Emails')
124 126
 
125
-    def __unicode__(self):
126
-        return _("Email to %(user)s with subject '%(subject)s'") % {
127
+    def __str__(self):
128
+        return _(u"Email to %(user)s with subject '%(subject)s'") % {
127 129
             'user': self.user.get_username(), 'subject': self.subject}
128 130
 
129 131
 
132
+@python_2_unicode_compatible
130 133
 class AbstractCommunicationEventType(models.Model):
131 134
     """
132 135
     A 'type' of communication.  Like a order confirmation email.
@@ -146,10 +149,17 @@ class AbstractCommunicationEventType(models.Model):
146 149
         help_text=_("This is just used for organisational purposes"))
147 150
 
148 151
     # We allow communication types to be categorised
149
-    ORDER_RELATED = _('Order related')
150
-    USER_RELATED = _('User related')
151
-    category = models.CharField(_('Category'), max_length=255,
152
-                                default=ORDER_RELATED)
152
+    # For backwards-compatibility, the choice values are quite verbose
153
+    ORDER_RELATED = 'Order related'
154
+    USER_RELATED = 'User related'
155
+    CATEGORY_CHOICES = (
156
+        (ORDER_RELATED, _('Order related')),
157
+        (USER_RELATED, _('User related'))
158
+    )
159
+
160
+    category = models.CharField(
161
+        _('Category'), max_length=255, default=ORDER_RELATED,
162
+        choices=CATEGORY_CHOICES)
153 163
 
154 164
     # Template content for emails
155 165
     # NOTE: There's an intentional distinction between None and ''. None
@@ -228,7 +238,7 @@ class AbstractCommunicationEventType(models.Model):
228 238
 
229 239
         return messages
230 240
 
231
-    def __unicode__(self):
241
+    def __str__(self):
232 242
         return self.name
233 243
 
234 244
     def is_order_related(self):
@@ -238,6 +248,7 @@ class AbstractCommunicationEventType(models.Model):
238 248
         return self.category == self.USER_RELATED
239 249
 
240 250
 
251
+@python_2_unicode_compatible
241 252
 class AbstractNotification(models.Model):
242 253
     recipient = models.ForeignKey(AUTH_USER_MODEL,
243 254
                                   related_name='notifications', db_index=True)
@@ -270,7 +281,7 @@ class AbstractNotification(models.Model):
270 281
         verbose_name = _('Notification')
271 282
         verbose_name_plural = _('Notifications')
272 283
 
273
-    def __unicode__(self):
284
+    def __str__(self):
274 285
         return self.subject
275 286
 
276 287
     def archive(self):

+ 14
- 3
oscar/apps/customer/auth_backends.py Visa fil

@@ -1,7 +1,9 @@
1
+import warnings
2
+
1 3
 from django.contrib.auth.backends import ModelBackend
2 4
 from django.core.exceptions import ImproperlyConfigured
3
-from oscar.apps.customer.utils import normalise_email
4 5
 
6
+from oscar.apps.customer.utils import normalise_email
5 7
 from oscar.core.compat import get_user_model
6 8
 
7 9
 User = get_user_model()
@@ -53,5 +55,14 @@ class EmailBackend(ModelBackend):
53 55
                 "password")
54 56
         return None
55 57
 
56
-# Deprecated in Oscar 0.8: Spelling
57
-Emailbackend = EmailBackend
58
+
59
+# Deprecated since Oscar 0.8 because of the spelling.
60
+class Emailbackend(EmailBackend):
61
+
62
+    def __init__(self):
63
+        warnings.warn(
64
+            "Oscar's auth backend EmailBackend has been renamed in Oscar 0.8 "
65
+            " and you're using the old name of Emailbackend. Please rename "
66
+            " all references; most likely in the AUTH_BACKENDS setting.",
67
+            DeprecationWarning)
68
+        super(Emailbackend, self).__init__()

+ 1
- 1
oscar/apps/customer/config.py Visa fil

@@ -8,5 +8,5 @@ class CustomerConfig(AppConfig):
8 8
     verbose_name = _('Customer')
9 9
 
10 10
     def ready(self):
11
-        from oscar.apps.customer import history  # noqa
11
+        from . import receivers  # noqa
12 12
         from .alerts import receivers  # noqa

+ 7
- 13
oscar/apps/customer/forms.py Visa fil

@@ -1,6 +1,5 @@
1 1
 import string
2 2
 import random
3
-from six.moves.urllib import parse
4 3
 
5 4
 from django import forms
6 5
 from django.conf import settings
@@ -8,6 +7,7 @@ from django.contrib.auth import forms as auth_forms
8 7
 from django.contrib.auth.forms import AuthenticationForm
9 8
 from django.contrib.sites.models import get_current_site
10 9
 from django.core.exceptions import ValidationError
10
+from django.utils.http import is_safe_url
11 11
 from django.utils.translation import ugettext_lazy as _
12 12
 from django.utils.translation import pgettext_lazy
13 13
 
@@ -107,11 +107,8 @@ class EmailAuthenticationForm(AuthenticationForm):
107 107
 
108 108
     def clean_redirect_url(self):
109 109
         url = self.cleaned_data['redirect_url'].strip()
110
-        if url:
111
-            # Ensure URL is not to a different host
112
-            host = parse.urlparse(url)[1]
113
-            if host and host == self.host:
114
-                return url
110
+        if url and is_safe_url(url):
111
+            return url
115 112
 
116 113
 
117 114
 class ConfirmPasswordForm(forms.Form):
@@ -172,12 +169,9 @@ class EmailUserCreationForm(forms.ModelForm):
172 169
 
173 170
     def clean_redirect_url(self):
174 171
         url = self.cleaned_data['redirect_url'].strip()
175
-        if not url:
176
-            return settings.LOGIN_REDIRECT_URL
177
-        host = parse.urlparse(url)[1]
178
-        if host and self.host and host != self.host:
179
-            return settings.LOGIN_REDIRECT_URL
180
-        return url
172
+        if url and is_safe_url(url):
173
+            return url
174
+        return settings.LOGIN_REDIRECT_URL
181 175
 
182 176
     def save(self, commit=True):
183 177
         user = super(EmailUserCreationForm, self).save(commit=False)
@@ -310,7 +304,7 @@ if Profile:
310 304
             super(UserAndProfileForm, self).__init__(*args, **kwargs)
311 305
 
312 306
             # Get profile field names to help with ordering later
313
-            profile_field_names = self.fields.keys()
307
+            profile_field_names = list(self.fields.keys())
314 308
 
315 309
             # Get user field names (we look for core user fields first)
316 310
             core_field_names = set([f.name for f in User._meta.fields])

+ 5
- 14
oscar/apps/customer/history.py Visa fil

@@ -2,11 +2,9 @@ import json
2 2
 
3 3
 from django.conf import settings
4 4
 from oscar.core.loading import get_model
5
-from django.dispatch import receiver
6 5
 
7 6
 from oscar.core.loading import get_class
8 7
 
9
-Product = get_model('catalogue', 'Product')
10 8
 product_viewed = get_class('catalogue.signals', 'product_viewed')
11 9
 
12 10
 
@@ -16,6 +14,11 @@ def get(request):
16 14
     """
17 15
     ids = extract(request)
18 16
 
17
+    # Needs to live in local scope because receivers in this module get
18
+    # registered during model initialisation
19
+    # TODO Move this back to global scope once Django < 1.7 support is removed
20
+    Product = get_model('catalogue', 'Product')
21
+
19 22
     # Reordering as the ID order gets messed up in the query
20 23
     product_dict = Product.browsable.in_bulk(ids)
21 24
     ids.reverse()
@@ -67,15 +70,3 @@ def update(product, request, response):
67 70
         json.dumps(updated_ids),
68 71
         max_age=settings.OSCAR_RECENTLY_VIEWED_COOKIE_LIFETIME,
69 72
         httponly=True)
70
-
71
-
72
-# Receivers
73
-
74
-@receiver(product_viewed)
75
-def receive_product_view(sender, product, user, request, response, **kwargs):
76
-    """
77
-    Receiver to handle viewing single product pages
78
-
79
-    Requires the request and response objects due to dependence on cookies
80
-    """
81
-    return update(product, request, response)

+ 30
- 30
oscar/apps/customer/migrations/0001_initial.py Visa fil

@@ -17,80 +17,80 @@ class Migration(migrations.Migration):
17 17
         migrations.CreateModel(
18 18
             name='CommunicationEventType',
19 19
             fields=[
20
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
21
-                ('code', oscar.models.fields.autoslugfield.AutoSlugField(populate_from=b'name', editable=False, max_length=128, separator='_', blank=True, help_text='Code used for looking up this event programmatically', unique=True, verbose_name='Code')),
22
-                ('name', models.CharField(help_text='This is just used for organisational purposes', max_length=255, verbose_name='Name')),
23
-                ('category', models.CharField(default='Order related', max_length=255, verbose_name='Category')),
24
-                ('email_subject_template', models.CharField(max_length=255, null=True, verbose_name='Email Subject Template', blank=True)),
25
-                ('email_body_template', models.TextField(null=True, verbose_name='Email Body Template', blank=True)),
26
-                ('email_body_html_template', models.TextField(help_text='HTML template', null=True, verbose_name='Email Body HTML Template', blank=True)),
27
-                ('sms_template', models.CharField(help_text='SMS template', max_length=170, null=True, verbose_name='SMS Template', blank=True)),
28
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
29
-                ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date Updated')),
20
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
21
+                ('code', oscar.models.fields.autoslugfield.AutoSlugField(editable=False, verbose_name='Code', blank=True, max_length=128, separator='_', populate_from='name', help_text='Code used for looking up this event programmatically', unique=True)),
22
+                ('name', models.CharField(verbose_name='Name', help_text='This is just used for organisational purposes', max_length=255)),
23
+                ('category', models.CharField(verbose_name='Category', default='Order related', max_length=255)),
24
+                ('email_subject_template', models.CharField(verbose_name='Email Subject Template', blank=True, null=True, max_length=255)),
25
+                ('email_body_template', models.TextField(verbose_name='Email Body Template', blank=True, null=True)),
26
+                ('email_body_html_template', models.TextField(verbose_name='Email Body HTML Template', blank=True, help_text='HTML template', null=True)),
27
+                ('sms_template', models.CharField(verbose_name='SMS Template', blank=True, help_text='SMS template', null=True, max_length=170)),
28
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
29
+                ('date_updated', models.DateTimeField(verbose_name='Date Updated', auto_now=True)),
30 30
             ],
31 31
             options={
32
-                'abstract': False,
33 32
                 'verbose_name': 'Communication event type',
34 33
                 'verbose_name_plural': 'Communication event types',
34
+                'abstract': False,
35 35
             },
36 36
             bases=(models.Model,),
37 37
         ),
38 38
         migrations.CreateModel(
39 39
             name='Email',
40 40
             fields=[
41
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
42
-                ('subject', models.TextField(max_length=255, verbose_name='Subject')),
41
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
42
+                ('subject', models.TextField(verbose_name='Subject', max_length=255)),
43 43
                 ('body_text', models.TextField(verbose_name='Body Text')),
44 44
                 ('body_html', models.TextField(verbose_name='Body HTML', blank=True)),
45
-                ('date_sent', models.DateTimeField(auto_now_add=True, verbose_name='Date Sent')),
45
+                ('date_sent', models.DateTimeField(verbose_name='Date Sent', auto_now_add=True)),
46 46
                 ('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL)),
47 47
             ],
48 48
             options={
49
-                'abstract': False,
50 49
                 'verbose_name': 'Email',
51 50
                 'verbose_name_plural': 'Emails',
51
+                'abstract': False,
52 52
             },
53 53
             bases=(models.Model,),
54 54
         ),
55 55
         migrations.CreateModel(
56 56
             name='Notification',
57 57
             fields=[
58
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
58
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
59 59
                 ('subject', models.CharField(max_length=255)),
60 60
                 ('body', models.TextField()),
61
-                ('category', models.CharField(max_length=255, blank=True)),
62
-                ('location', models.CharField(default=b'Inbox', max_length=32, choices=[(b'Inbox', 'Inbox'), (b'Archive', 'Archive')])),
61
+                ('category', models.CharField(blank=True, max_length=255)),
62
+                ('location', models.CharField(choices=[('Inbox', 'Inbox'), ('Archive', 'Archive')], default='Inbox', max_length=32)),
63 63
                 ('date_sent', models.DateTimeField(auto_now_add=True)),
64
-                ('date_read', models.DateTimeField(null=True, blank=True)),
64
+                ('date_read', models.DateTimeField(blank=True, null=True)),
65 65
                 ('recipient', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
66 66
                 ('sender', models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True)),
67 67
             ],
68 68
             options={
69
-                'ordering': (b'-date_sent',),
70
-                'abstract': False,
71 69
                 'verbose_name': 'Notification',
72 70
                 'verbose_name_plural': 'Notifications',
71
+                'ordering': ('-date_sent',),
72
+                'abstract': False,
73 73
             },
74 74
             bases=(models.Model,),
75 75
         ),
76 76
         migrations.CreateModel(
77 77
             name='ProductAlert',
78 78
             fields=[
79
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
80
-                ('email', models.EmailField(db_index=True, max_length=75, verbose_name='Email', blank=True)),
81
-                ('key', models.CharField(db_index=True, max_length=128, verbose_name='Key', blank=True)),
82
-                ('status', models.CharField(default=b'Active', max_length=20, verbose_name='Status', choices=[(b'Unconfirmed', 'Not yet confirmed'), (b'Active', 'Active'), (b'Cancelled', 'Cancelled'), (b'Closed', 'Closed')])),
83
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
84
-                ('date_confirmed', models.DateTimeField(null=True, verbose_name='Date confirmed', blank=True)),
85
-                ('date_cancelled', models.DateTimeField(null=True, verbose_name='Date cancelled', blank=True)),
86
-                ('date_closed', models.DateTimeField(null=True, verbose_name='Date closed', blank=True)),
79
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
80
+                ('email', models.EmailField(verbose_name='Email', blank=True, db_index=True, max_length=75)),
81
+                ('key', models.CharField(verbose_name='Key', blank=True, db_index=True, max_length=128)),
82
+                ('status', models.CharField(verbose_name='Status', choices=[('Unconfirmed', 'Not yet confirmed'), ('Active', 'Active'), ('Cancelled', 'Cancelled'), ('Closed', 'Closed')], default='Active', max_length=20)),
83
+                ('date_created', models.DateTimeField(verbose_name='Date created', auto_now_add=True)),
84
+                ('date_confirmed', models.DateTimeField(verbose_name='Date confirmed', blank=True, null=True)),
85
+                ('date_cancelled', models.DateTimeField(verbose_name='Date cancelled', blank=True, null=True)),
86
+                ('date_closed', models.DateTimeField(verbose_name='Date closed', blank=True, null=True)),
87 87
                 ('product', models.ForeignKey(to='catalogue.Product')),
88 88
                 ('user', models.ForeignKey(verbose_name='User', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
89 89
             ],
90 90
             options={
91
-                'abstract': False,
92 91
                 'verbose_name': 'Product alert',
93 92
                 'verbose_name_plural': 'Product alerts',
93
+                'abstract': False,
94 94
             },
95 95
             bases=(models.Model,),
96 96
         ),

+ 19
- 0
oscar/apps/customer/migrations/0002_auto_20140808_1205.py Visa fil

@@ -0,0 +1,19 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
3
+
4
+from django.db import models, migrations
5
+
6
+
7
+class Migration(migrations.Migration):
8
+
9
+    dependencies = [
10
+        ('customer', '0001_initial'),
11
+    ]
12
+
13
+    operations = [
14
+        migrations.AlterField(
15
+            model_name='communicationeventtype',
16
+            name='category',
17
+            field=models.CharField(choices=[('Order related', 'Order related'), ('User related', 'User related')], verbose_name='Category', max_length=255, default='Order related'),
18
+        ),
19
+    ]

+ 1
- 1
oscar/apps/customer/models.py Visa fil

@@ -26,5 +26,5 @@ if not is_model_registered('customer', 'ProductAlert'):
26 26
 
27 27
 
28 28
 if django.VERSION < (1, 7):
29
-    from oscar.apps.customer.history import *  # noqa
29
+    from .receivers import *  # noqa
30 30
     from .alerts import receivers  # noqa

+ 4
- 6
oscar/apps/customer/notifications/views.py Visa fil

@@ -1,12 +1,11 @@
1
-from django.core.urlresolvers import reverse
2 1
 from django.utils.html import strip_tags
3 2
 from django.utils.translation import ugettext_lazy as _, ungettext
4 3
 from django.utils.timezone import now
5 4
 from django.contrib import messages
6
-from django import http
7 5
 from django.views import generic
8
-from oscar.core.loading import get_model
9 6
 
7
+from oscar.core.loading import get_model
8
+from oscar.core.utils import redirect_to_referrer
10 9
 from oscar.apps.customer.mixins import PageTitleMixin
11 10
 from oscar.views.generic import BulkEditMixin
12 11
 
@@ -82,9 +81,8 @@ class UpdateView(BulkEditMixin, generic.RedirectView):
82 81
             recipient=self.request.user).in_bulk(ids)
83 82
 
84 83
     def get_success_response(self):
85
-        default = reverse('customer:notifications-inbox')
86
-        return http.HttpResponseRedirect(
87
-            self.request.META.get('HTTP_REFERER', default))
84
+        return redirect_to_referrer(
85
+            self.request.META, 'customer:notifications-inbox')
88 86
 
89 87
     def archive(self, request, notifications):
90 88
         for notification in notifications:

+ 17
- 0
oscar/apps/customer/receivers.py Visa fil

@@ -0,0 +1,17 @@
1
+from django.dispatch import receiver
2
+
3
+from oscar.core.loading import get_class
4
+
5
+from . import history
6
+
7
+product_viewed = get_class('catalogue.signals', 'product_viewed')
8
+
9
+
10
+@receiver(product_viewed)
11
+def receive_product_view(sender, product, user, request, response, **kwargs):
12
+    """
13
+    Receiver to handle viewing single product pages
14
+
15
+    Requires the request and response objects due to dependence on cookies
16
+    """
17
+    return history.update(product, request, response)

+ 4
- 4
oscar/apps/customer/utils.py Visa fil

@@ -43,10 +43,10 @@ class Dispatcher(object):
43 43
         else:
44 44
             self.dispatch_user_messages(order.user, messages)
45 45
 
46
-        # Create order comms event for audit
47
-        if event_type:
48
-            CommunicationEvent._default_manager.create(order=order,
49
-                                                       event_type=event_type)
46
+        # Create order communications event for audit
47
+        if event_type is not None:
48
+            CommunicationEvent._default_manager.create(
49
+                order=order, event_type=event_type)
50 50
 
51 51
     def dispatch_user_messages(self, user, messages):
52 52
         """

+ 15
- 19
oscar/apps/customer/views.py Visa fil

@@ -9,10 +9,11 @@ from django.contrib.auth import logout as auth_logout, login as auth_login
9 9
 from django.contrib.sites.models import get_current_site
10 10
 from django.conf import settings
11 11
 
12
-from oscar.core.loading import get_model
12
+from oscar.core.utils import safe_referrer
13 13
 from oscar.views.generic import PostActionMixin
14 14
 from oscar.apps.customer.utils import get_password_reset_url
15
-from oscar.core.loading import get_class, get_profile_class, get_classes
15
+from oscar.core.loading import (
16
+    get_class, get_profile_class, get_classes, get_model)
16 17
 from oscar.core.compat import get_user_model
17 18
 from . import signals
18 19
 
@@ -59,7 +60,7 @@ class AccountRegistrationView(RegisterUserMixin, generic.FormView):
59 60
 
60 61
     def get(self, request, *args, **kwargs):
61 62
         if request.user.is_authenticated():
62
-            return http.HttpResponseRedirect(settings.LOGIN_REDIRECT_URL)
63
+            return redirect(settings.LOGIN_REDIRECT_URL)
63 64
         return super(AccountRegistrationView, self).get(
64 65
             request, *args, **kwargs)
65 66
 
@@ -78,13 +79,12 @@ class AccountRegistrationView(RegisterUserMixin, generic.FormView):
78 79
     def get_context_data(self, *args, **kwargs):
79 80
         ctx = super(AccountRegistrationView, self).get_context_data(
80 81
             *args, **kwargs)
81
-        ctx['cancel_url'] = self.request.META.get('HTTP_REFERER', None)
82
+        ctx['cancel_url'] = safe_referrer(self.request.META, '')
82 83
         return ctx
83 84
 
84 85
     def form_valid(self, form):
85 86
         self.register_user(form)
86
-        return http.HttpResponseRedirect(
87
-            form.cleaned_data['redirect_url'])
87
+        return redirect(form.cleaned_data['redirect_url'])
88 88
 
89 89
 
90 90
 class AccountAuthView(RegisterUserMixin, generic.TemplateView):
@@ -100,7 +100,7 @@ class AccountAuthView(RegisterUserMixin, generic.TemplateView):
100 100
 
101 101
     def get(self, request, *args, **kwargs):
102 102
         if request.user.is_authenticated():
103
-            return http.HttpResponseRedirect(settings.LOGIN_REDIRECT_URL)
103
+            return redirect(settings.LOGIN_REDIRECT_URL)
104 104
         return super(AccountAuthView, self).get(
105 105
             request, *args, **kwargs)
106 106
 
@@ -160,8 +160,7 @@ class AccountAuthView(RegisterUserMixin, generic.TemplateView):
160 160
             msg = self.get_login_success_message(form)
161 161
             messages.success(self.request, msg)
162 162
 
163
-            url = self.get_login_success_url(form)
164
-            return http.HttpResponseRedirect(url)
163
+            return redirect(self.get_login_success_url(form))
165 164
 
166 165
         ctx = self.get_context_data(login_form=form)
167 166
         return self.render_to_response(ctx)
@@ -209,8 +208,7 @@ class AccountAuthView(RegisterUserMixin, generic.TemplateView):
209 208
             msg = self.get_registration_success_message(form)
210 209
             messages.success(self.request, msg)
211 210
 
212
-            url = self.get_registration_success_url(form)
213
-            return http.HttpResponseRedirect(url)
211
+            return redirect(self.get_registration_success_url(form))
214 212
 
215 213
         ctx = self.get_context_data(registration_form=form)
216 214
         return self.render_to_response(ctx)
@@ -354,7 +352,7 @@ class ProfileDeleteView(PageTitleMixin, generic.FormView):
354 352
         messages.success(
355 353
             self.request,
356 354
             _("Your profile has now been deleted. Thanks for using the site."))
357
-        return http.HttpResponseRedirect(self.get_success_url())
355
+        return redirect(self.get_success_url())
358 356
 
359 357
 
360 358
 class ChangePasswordView(PageTitleMixin, generic.FormView):
@@ -451,9 +449,8 @@ class OrderHistoryView(PageTitleMixin, generic.ListView):
451 449
                 except Order.DoesNotExist:
452 450
                     pass
453 451
                 else:
454
-                    return http.HttpResponseRedirect(
455
-                        reverse('customer:order',
456
-                                kwargs={'order_number': order.number}))
452
+                    return redirect(
453
+                        'customer:order', order_number=order.number)
457 454
         else:
458 455
             self.form = self.form_class()
459 456
         return super(OrderHistoryView, self).get(request, *args, **kwargs)
@@ -553,9 +550,8 @@ class OrderLineView(PostActionMixin, generic.DetailView):
553 550
         return order.lines.get(id=self.kwargs['line_id'])
554 551
 
555 552
     def do_reorder(self, line):
556
-        self.response = http.HttpResponseRedirect(
557
-            reverse('customer:order',
558
-                    args=(int(self.kwargs['order_number']),)))
553
+        self.response = redirect(
554
+            'customer:order', int(self.kwargs['order_number']))
559 555
         basket = self.request.basket
560 556
 
561 557
         line_available_to_reorder, reason = line.is_available_to_reorder(
@@ -567,7 +563,7 @@ class OrderLineView(PostActionMixin, generic.DetailView):
567 563
 
568 564
         # We need to pass response to the get_or_create... method
569 565
         # as a new basket might need to be created
570
-        self.response = http.HttpResponseRedirect(reverse('basket:summary'))
566
+        self.response = redirect('basket:summary')
571 567
 
572 568
         # Convert line attributes into basket options
573 569
         options = []

+ 14
- 17
oscar/apps/customer/wishlists/views.py Visa fil

@@ -3,15 +3,15 @@ from django.contrib import messages
3 3
 from django.core.exceptions import (
4 4
     ObjectDoesNotExist, MultipleObjectsReturned, PermissionDenied)
5 5
 from django.core.urlresolvers import reverse
6
-from oscar.core.loading import get_model
7
-from django.http import Http404, HttpResponseRedirect
8
-from django.shortcuts import get_object_or_404
6
+from django.http import Http404
7
+from django.shortcuts import get_object_or_404, redirect
9 8
 from django.views.generic import (ListView, CreateView, UpdateView, DeleteView,
10 9
                                   View, FormView)
11 10
 from django.utils.translation import ugettext_lazy as _
12 11
 
13 12
 from oscar.apps.customer.mixins import PageTitleMixin
14
-from oscar.core.loading import get_classes
13
+from oscar.core.loading import get_classes, get_model
14
+from oscar.core.utils import redirect_to_referrer, safe_referrer
15 15
 
16 16
 WishList = get_model('wishlists', 'WishList')
17 17
 Line = get_model('wishlists', 'Line')
@@ -76,8 +76,7 @@ class WishListDetailView(PageTitleMixin, FormView):
76 76
             else:
77 77
                 subform.save()
78 78
         messages.success(self.request, _('Quantities updated.'))
79
-        return HttpResponseRedirect(reverse('customer:wishlists-detail',
80
-                                            kwargs={'key': self.object.key}))
79
+        return redirect('customer:wishlists-detail', key=self.object.key)
81 80
 
82 81
 
83 82
 class WishListCreateView(PageTitleMixin, CreateView):
@@ -101,7 +100,7 @@ class WishListCreateView(PageTitleMixin, CreateView):
101 100
             except ObjectDoesNotExist:
102 101
                 messages.error(
103 102
                     request, _("The requested product no longer exists"))
104
-                return HttpResponseRedirect(reverse('wishlists-create'))
103
+                return redirect('wishlists-create')
105 104
         return super(WishListCreateView, self).dispatch(
106 105
             request, *args, **kwargs)
107 106
 
@@ -125,7 +124,7 @@ class WishListCreateView(PageTitleMixin, CreateView):
125 124
         else:
126 125
             msg = _("Your wishlist has been created")
127 126
         messages.success(self.request, msg)
128
-        return HttpResponseRedirect(wishlist.get_absolute_url())
127
+        return redirect(wishlist.get_absolute_url())
129 128
 
130 129
 
131 130
 class WishListCreateWithProductView(View):
@@ -147,8 +146,7 @@ class WishListCreateWithProductView(View):
147 146
         messages.success(
148 147
             request, _("%(title)s has been added to your wishlist") % {
149 148
                 'title': product.get_title()})
150
-        return HttpResponseRedirect(request.META.get(
151
-            'HTTP_REFERER', wishlist.get_absolute_url()))
149
+        return redirect_to_referrer(request.META, wishlist.get_absolute_url())
152 150
 
153 151
 
154 152
 class WishListUpdateView(PageTitleMixin, UpdateView):
@@ -237,9 +235,8 @@ class WishListAddProduct(View):
237 235
         self.wishlist.add(self.product)
238 236
         msg = _("'%s' was added to your wish list.") % self.product.get_title()
239 237
         messages.success(self.request, msg)
240
-        return HttpResponseRedirect(
241
-            self.request.META.get('HTTP_REFERER',
242
-                                  self.product.get_absolute_url()))
238
+        return redirect_to_referrer(
239
+            self.request.META, self.product.get_absolute_url())
243 240
 
244 241
 
245 242
 class LineMixin(object):
@@ -292,8 +289,9 @@ class WishListRemoveProduct(LineMixin, PageTitleMixin, DeleteView):
292 289
 
293 290
         # We post directly to this view on product pages; and should send the
294 291
         # user back there if that was the case
295
-        referrer = self.request.META.get('HTTP_REFERER', '')
296
-        if self.product and self.product.get_absolute_url() in referrer:
292
+        referrer = safe_referrer(self.request.META, '')
293
+        if (referrer and self.product and
294
+                self.product.get_absolute_url() in referrer):
297 295
             return referrer
298 296
         else:
299 297
             return reverse(
@@ -324,5 +322,4 @@ class WishListMoveProductToAnotherWishList(LineMixin, View):
324 322
 
325 323
         default_url = reverse(
326 324
             'customer:wishlists-detail', kwargs={'key': self.wishlist.key})
327
-        return HttpResponseRedirect(self.request.META.get(
328
-            'HTTP_REFERER', default_url))
325
+        return redirect_to_referrer(self.request.META, default_url)

+ 58
- 0
oscar/apps/dashboard/catalogue/tables.py Visa fil

@@ -0,0 +1,58 @@
1
+from django.utils.safestring import mark_safe
2
+from django.utils.translation import ugettext_lazy as _
3
+
4
+from django_tables2 import Table, Column, LinkColumn, TemplateColumn, A
5
+
6
+from oscar.core.loading import get_class, get_model
7
+
8
+DashboardTable = get_class('dashboard.tables', 'DashboardTable')
9
+Product = get_model('catalogue', 'Product')
10
+Category = get_model('catalogue', 'Category')
11
+
12
+
13
+class ProductTable(Table):
14
+    title = TemplateColumn(
15
+        template_name='dashboard/catalogue/product_row_title.html',
16
+        order_by='title', accessor=A('get_title'))
17
+    image = TemplateColumn(
18
+        template_name='dashboard/catalogue/product_row_image.html',
19
+        orderable=False)
20
+    product_class = Column(verbose_name=_("Type"),
21
+                           accessor=A('get_product_class.name'),
22
+                           order_by=('product_class__name'))
23
+    parent = LinkColumn('dashboard:catalogue-product',
24
+                        verbose_name=_("Parent"), args=[A('parent.pk')],
25
+                        accessor=A('parent.title'))
26
+    children = Column(accessor=A('children.count'), orderable=False)
27
+    stock_records = Column(accessor=A('stockrecords.count'), orderable=False)
28
+    actions = TemplateColumn(
29
+        template_name='dashboard/catalogue/product_row_actions.html',
30
+        orderable=False)
31
+
32
+    class Meta(DashboardTable.Meta):
33
+        model = Product
34
+        fields = ('upc', 'date_created')
35
+        sequence = ('title', 'upc', 'image', 'product_class',
36
+                    'parent', 'children', 'stock_records', '...', 'date_created', 'actions')
37
+        order_by = '-date_created'
38
+
39
+
40
+class CategoryTable(Table):
41
+    name = LinkColumn('dashboard:catalogue-category-update', args=[A('pk')])
42
+    description = TemplateColumn(
43
+        template_code='{{ record.description|default:""|striptags'
44
+                      '|cut:"&nbsp;"|truncatewords:6 }}')
45
+    # mark_safe is needed because of
46
+    # https://github.com/bradleyayers/django-tables2/issues/187
47
+    num_children = LinkColumn(
48
+        'dashboard:catalogue-category-detail-list', args=[A('pk')],
49
+        verbose_name=mark_safe(_('Number of child categories')),
50
+        accessor='get_num_children',
51
+        orderable=False)
52
+    actions = TemplateColumn(
53
+        template_name='dashboard/catalogue/category_row_actions.html',
54
+        orderable=False)
55
+
56
+    class Meta(DashboardTable.Meta):
57
+        model = Category
58
+        fields = ('name', 'description')

+ 37
- 16
oscar/apps/dashboard/catalogue/views.py Visa fil

@@ -1,16 +1,16 @@
1
-from django.shortcuts import get_object_or_404, redirect
2
-import six
3
-
4 1
 from django.views import generic
5 2
 from django.db.models import Q
6 3
 from django.http import HttpResponseRedirect
7 4
 from django.contrib import messages
8 5
 from django.core.urlresolvers import reverse
9 6
 from django.utils.translation import ugettext_lazy as _
7
+from django.shortcuts import get_object_or_404, redirect
10 8
 from django.template.loader import render_to_string
11 9
 
12 10
 from oscar.core.loading import get_classes, get_model
13
-from oscar.views import sort_queryset
11
+
12
+from django_tables2 import SingleTableMixin
13
+
14 14
 from oscar.views.generic import ObjectLookupView
15 15
 
16 16
 (ProductForm,
@@ -34,6 +34,9 @@ from oscar.views.generic import ObjectLookupView
34 34
                    'ProductCategoryFormSet',
35 35
                    'ProductImageFormSet',
36 36
                    'ProductRecommendationFormSet'))
37
+ProductTable, CategoryTable \
38
+    = get_classes('dashboard.catalogue.tables',
39
+                  ('ProductTable', 'CategoryTable'))
37 40
 Product = get_model('catalogue', 'Product')
38 41
 Category = get_model('catalogue', 'Category')
39 42
 ProductImage = get_model('catalogue', 'ProductImage')
@@ -57,20 +60,19 @@ def filter_products(queryset, user):
57 60
     return queryset.filter(stockrecords__partner__users__pk=user.pk).distinct()
58 61
 
59 62
 
60
-class ProductListView(generic.ListView):
63
+class ProductListView(SingleTableMixin, generic.TemplateView):
61 64
     """
62 65
     Dashboard view of the product list.
63 66
     Supports the permission-based dashboard.
64 67
     """
65 68
 
66 69
     template_name = 'dashboard/catalogue/product_list.html'
67
-    model = Product
68
-    context_object_name = 'products'
69 70
     form_class = ProductSearchForm
70 71
     productclass_form_class = ProductClassSelectForm
71 72
     description_template = _(u'Products %(upc_filter)s %(title_filter)s')
72
-    paginate_by = 20
73 73
     recent_products = 5
74
+    table_class = ProductTable
75
+    context_table_name = 'products'
74 76
 
75 77
     def get_context_data(self, **kwargs):
76 78
         ctx = super(ProductListView, self).get_context_data(**kwargs)
@@ -85,6 +87,15 @@ class ProductListView(generic.ListView):
85 87
 
86 88
         return ctx
87 89
 
90
+    def get_table(self, **kwargs):
91
+        if 'recently_edited' in self.request.GET:
92
+            kwargs.update(dict(orderable=False))
93
+
94
+        return super(ProductListView, self).get_table(**kwargs)
95
+
96
+    def get_table_pagination(self):
97
+        return dict(per_page=20)
98
+
88 99
     def filter_queryset(self, queryset):
89 100
         """
90 101
         Apply any filters to restrict the products that appear on the list
@@ -107,10 +118,7 @@ class ProductListView(generic.ListView):
107 118
             # Just show recently edited
108 119
             queryset = queryset.order_by('-date_updated')
109 120
             queryset = queryset[:self.recent_products]
110
-        else:
111
-            # Allow sorting when all
112
-            queryset = sort_queryset(queryset, self.request,
113
-                                     ['title'], '-date_created')
121
+
114 122
         return queryset
115 123
 
116 124
     def apply_search(self, queryset):
@@ -267,7 +275,7 @@ class ProductCreateUpdateView(generic.UpdateView):
267 275
         ctx['parent'] = self.parent
268 276
         ctx['title'] = self.get_page_title()
269 277
 
270
-        for ctx_name, formset_class in six.iteritems(self.formsets):
278
+        for ctx_name, formset_class in self.formsets.items():
271 279
             if ctx_name not in ctx:
272 280
                 ctx[ctx_name] = formset_class(self.product_class,
273 281
                                               self.request.user,
@@ -306,7 +314,7 @@ class ProductCreateUpdateView(generic.UpdateView):
306 314
             self.object = form.save()
307 315
 
308 316
         formsets = {}
309
-        for ctx_name, formset_class in six.iteritems(self.formsets):
317
+        for ctx_name, formset_class in self.formsets.items():
310 318
             formsets[ctx_name] = formset_class(self.product_class,
311 319
                                                self.request.user,
312 320
                                                self.request.POST,
@@ -401,6 +409,7 @@ class ProductCreateUpdateView(generic.UpdateView):
401 409
             {
402 410
                 'product': self.object,
403 411
                 'creating': self.creating,
412
+                'request': self.request
404 413
             })
405 414
         messages.success(self.request, msg, extra_tags="safe noicon")
406 415
 
@@ -521,25 +530,37 @@ class StockAlertListView(generic.ListView):
521 530
         return self.model.objects.all()
522 531
 
523 532
 
524
-class CategoryListView(generic.TemplateView):
533
+class CategoryListView(SingleTableMixin, generic.TemplateView):
525 534
     template_name = 'dashboard/catalogue/category_list.html'
535
+    table_class = CategoryTable
536
+    context_table_name = 'categories'
537
+
538
+    def get_queryset(self):
539
+        return Category.get_root_nodes()
526 540
 
527 541
     def get_context_data(self, *args, **kwargs):
528 542
         ctx = super(CategoryListView, self).get_context_data(*args, **kwargs)
529 543
         ctx['child_categories'] = Category.get_root_nodes()
544
+        ctx['queryset_description'] = _("Categories")
530 545
         return ctx
531 546
 
532 547
 
533
-class CategoryDetailListView(generic.DetailView):
548
+class CategoryDetailListView(SingleTableMixin, generic.DetailView):
534 549
     template_name = 'dashboard/catalogue/category_list.html'
535 550
     model = Category
536 551
     context_object_name = 'category'
552
+    table_class = CategoryTable
553
+    context_table_name = 'categories'
554
+
555
+    def get_table_data(self):
556
+        return self.object.get_children()
537 557
 
538 558
     def get_context_data(self, *args, **kwargs):
539 559
         ctx = super(CategoryDetailListView, self).get_context_data(*args,
540 560
                                                                    **kwargs)
541 561
         ctx['child_categories'] = self.object.get_children()
542 562
         ctx['ancestors'] = self.object.get_ancestors()
563
+        ctx['queryset_description'] = _("Categories")
543 564
         return ctx
544 565
 
545 566
 

+ 8
- 5
oscar/apps/dashboard/communications/forms.py Visa fil

@@ -1,11 +1,11 @@
1
+from django.utils import six
2
+
1 3
 from django import forms
2 4
 from oscar.core.loading import get_model
3 5
 from django.template import Template, TemplateSyntaxError
4 6
 from django.utils.translation import ugettext_lazy as _
5 7
 from oscar.apps.customer.utils import normalise_email
6 8
 
7
-from oscar.forms import widgets
8
-
9 9
 CommunicationEventType = get_model('customer', 'CommunicationEventType')
10 10
 Order = get_model('order', 'Order')
11 11
 
@@ -18,7 +18,7 @@ class CommunicationEventTypeForm(forms.ModelForm):
18 18
         widget=forms.widgets.Textarea(attrs={'class': 'plain'}))
19 19
     email_body_html_template = forms.CharField(
20 20
         label=_("Email body HTML template"), required=True,
21
-        widget=widgets.WYSIWYGTextArea)
21
+        widget=forms.Textarea)
22 22
 
23 23
     preview_order_number = forms.CharField(
24 24
         label=_("Order number"), required=False)
@@ -37,7 +37,7 @@ class CommunicationEventTypeForm(forms.ModelForm):
37 37
         try:
38 38
             Template(value)
39 39
         except TemplateSyntaxError as e:
40
-            raise forms.ValidationError(e.message)
40
+            raise forms.ValidationError(six.text_type(e))
41 41
 
42 42
     def clean_email_subject_template(self):
43 43
         subject = self.cleaned_data['email_subject_template']
@@ -84,4 +84,7 @@ class CommunicationEventTypeForm(forms.ModelForm):
84 84
 
85 85
     class Meta:
86 86
         model = CommunicationEventType
87
-        exclude = ('code', 'category', 'sms_template')
87
+        fields = [
88
+            'name', 'email_subject_template', 'email_body_template',
89
+            'email_body_html_template', 'preview_order_number', 'preview_email'
90
+        ]

+ 4
- 2
oscar/apps/dashboard/communications/views.py Visa fil

@@ -1,3 +1,5 @@
1
+from django.utils import six
2
+
1 3
 from django.contrib import messages
2 4
 from django.contrib.sites.models import get_current_site
3 5
 from oscar.core.loading import get_model
@@ -56,7 +58,7 @@ class UpdateView(generic.UpdateView):
56 58
         try:
57 59
             msgs = commtype.get_messages(commtype_ctx)
58 60
         except TemplateSyntaxError as e:
59
-            form.errors['__all__'] = form.error_class([e.message])
61
+            form.errors['__all__'] = form.error_class([six.text_type(e)])
60 62
             return self.render_to_response(ctx)
61 63
 
62 64
         ctx['show_preview'] = True
@@ -72,7 +74,7 @@ class UpdateView(generic.UpdateView):
72 74
         try:
73 75
             msgs = commtype.get_messages(commtype_ctx)
74 76
         except TemplateSyntaxError as e:
75
-            form.errors['__all__'] = form.error_class([e.message])
77
+            form.errors['__all__'] = form.error_class([six.text_type(e)])
76 78
             return self.render_to_response(ctx)
77 79
 
78 80
         email = form.cleaned_data['preview_email']

+ 4
- 3
oscar/apps/dashboard/offers/forms.py Visa fil

@@ -1,9 +1,10 @@
1 1
 import datetime
2
+from django.utils import six
2 3
 
3 4
 from django import forms
4
-from oscar.core.loading import get_model
5 5
 from django.utils.translation import ugettext_lazy as _
6 6
 
7
+from oscar.core.loading import get_model
7 8
 from oscar.forms import widgets
8 9
 
9 10
 ConditionalOffer = get_model('offer', 'ConditionalOffer')
@@ -60,7 +61,7 @@ class ConditionForm(forms.ModelForm):
60 61
             proxy_class=None)
61 62
         if len(custom_conditions) > 0:
62 63
             # Initialise custom_condition field
63
-            choices = [(c.id, c.__unicode__()) for c in custom_conditions]
64
+            choices = [(c.id, six.text_type(c)) for c in custom_conditions]
64 65
             choices.insert(0, ('', ' --------- '))
65 66
             self.fields['custom_condition'].choices = choices
66 67
             condition = kwargs.get('instance')
@@ -114,7 +115,7 @@ class BenefitForm(forms.ModelForm):
114 115
             proxy_class=None)
115 116
         if len(custom_benefits) > 0:
116 117
             # Initialise custom_benefit field
117
-            choices = [(c.id, c.__unicode__()) for c in custom_benefits]
118
+            choices = [(c.id, six.text_type(c)) for c in custom_benefits]
118 119
             choices.insert(0, ('', ' --------- '))
119 120
             self.fields['custom_benefit'].choices = choices
120 121
             benefit = kwargs.get('instance')

+ 1
- 3
oscar/apps/dashboard/orders/views.py Visa fil

@@ -655,9 +655,7 @@ class OrderDetailView(DetailView):
655 655
 
656 656
         # If no amount passed, then we add up the total of the selected lines
657 657
         if not amount_str:
658
-            amount = D('0.00')
659
-            for line, quantity in zip(lines, quantities):
660
-                amount += int(quantity) * line.line_price_incl_tax
658
+            amount = sum([line.line_price_incl_tax for line in lines])
661 659
         else:
662 660
             try:
663 661
                 amount = D(amount_str)

+ 7
- 0
oscar/apps/dashboard/tables.py Visa fil

@@ -0,0 +1,7 @@
1
+from django_tables2 import Table
2
+
3
+
4
+class DashboardTable(Table):
5
+    class Meta:
6
+        template = 'dashboard/table.html'
7
+        attrs = {'class': 'table table-striped table-bordered'}

+ 25
- 0
oscar/apps/dashboard/users/tables.py Visa fil

@@ -0,0 +1,25 @@
1
+from django_tables2 import Table, LinkColumn, TemplateColumn, Column, A
2
+
3
+from oscar.core.loading import get_class
4
+
5
+DashboardTable = get_class('dashboard.tables', 'DashboardTable')
6
+
7
+
8
+class UserTable(Table):
9
+    check = TemplateColumn(
10
+        template_name='dashboard/users/user_row_checkbox.html',
11
+        verbose_name=' ', orderable=False)
12
+    email = LinkColumn('dashboard:user-detail', args=[A('id')],
13
+                       accessor='email')
14
+    name = Column(accessor='get_full_name',
15
+                  order_by=('last_name', 'first_name'))
16
+    active = Column(accessor='is_active')
17
+    staff = Column(accessor='is_staff')
18
+    date_registered = Column(accessor='date_joined')
19
+    num_orders = Column(accessor='orders.count', orderable=False)
20
+    actions = TemplateColumn(
21
+        template_name='dashboard/users/user_row_actions.html',
22
+        verbose_name=' ')
23
+
24
+    class Meta(DashboardTable.Meta):
25
+        template = 'dashboard/users/table.html'

+ 40
- 15
oscar/apps/dashboard/users/views.py Visa fil

@@ -4,10 +4,13 @@ from django.shortcuts import redirect
4 4
 from django.utils.translation import ugettext_lazy as _
5 5
 from django.core.urlresolvers import reverse
6 6
 from django.views.generic import ListView, DetailView, DeleteView, \
7
-    UpdateView, FormView
7
+    UpdateView, FormView, TemplateView
8 8
 from django.views.generic.detail import SingleObjectMixin
9
-from oscar.apps.customer.utils import normalise_email
9
+from django.views.generic.edit import FormMixin
10
+
11
+from django_tables2 import SingleTableMixin
10 12
 
13
+from oscar.apps.customer.utils import normalise_email
11 14
 from oscar.views.generic import BulkEditMixin
12 15
 from oscar.core.compat import get_user_model
13 16
 from oscar.core.loading import get_class, get_classes, get_model
@@ -16,39 +19,60 @@ UserSearchForm, ProductAlertSearchForm, ProductAlertUpdateForm = get_classes(
16 19
     'dashboard.users.forms', ('UserSearchForm', 'ProductAlertSearchForm',
17 20
                               'ProductAlertUpdateForm'))
18 21
 PasswordResetForm = get_class('customer.forms', 'PasswordResetForm')
22
+UserTable = get_class('dashboard.users.tables', 'UserTable')
19 23
 ProductAlert = get_model('customer', 'ProductAlert')
20 24
 User = get_user_model()
21 25
 
22 26
 
23
-class IndexView(BulkEditMixin, ListView):
27
+class IndexView(BulkEditMixin, SingleTableMixin, FormMixin, TemplateView):
24 28
     template_name = 'dashboard/users/index.html'
25
-    paginate_by = 25
29
+    table_pagination = True
26 30
     model = User
27 31
     actions = ('make_active', 'make_inactive', )
28 32
     form_class = UserSearchForm
33
+    table_class = UserTable
34
+    context_table_name = 'users'
29 35
     desc_template = _('%(main_filter)s %(email_filter)s %(name_filter)s')
30 36
     description = ''
31
-    context_object_name = 'user_list'
37
+
38
+    def dispatch(self, request, *args, **kwargs):
39
+        form_class = self.get_form_class()
40
+        self.form = self.get_form(form_class)
41
+        return super(IndexView, self).dispatch(request, *args, **kwargs)
42
+
43
+    def get_form_kwargs(self):
44
+        """
45
+        Only bind search form if it was submitted.
46
+        """
47
+        kwargs = super(IndexView, self).get_form_kwargs()
48
+
49
+        if 'search' in self.request.GET:
50
+            kwargs.update({
51
+                'data': self.request.GET,
52
+            })
53
+
54
+        return kwargs
32 55
 
33 56
     def get_queryset(self):
34 57
         queryset = self.model.objects.all().order_by('-date_joined')
58
+        return self.apply_search(queryset)
59
+
60
+    def apply_search(self, queryset):
61
+        # Set initial queryset description, used for template context
35 62
         self.desc_ctx = {
36 63
             'main_filter': _('All users'),
37 64
             'email_filter': '',
38 65
             'name_filter': '',
39 66
         }
40
-
41
-        if 'email' not in self.request.GET:
42
-            self.form = self.form_class()
67
+        if self.form.is_valid():
68
+            return self.apply_search_filters(queryset, self.form.cleaned_data)
69
+        else:
43 70
             return queryset
44 71
 
45
-        self.form = self.form_class(self.request.GET)
46
-
47
-        if not self.form.is_valid():
48
-            return queryset
49
-
50
-        data = self.form.cleaned_data
51
-
72
+    def apply_search_filters(self, queryset, data):
73
+        """
74
+        Function is split out to allow customisation with little boilerplate.
75
+        """
52 76
         if data['email']:
53 77
             email = normalise_email(data['email'])
54 78
             queryset = queryset.filter(email__istartswith=email)
@@ -74,6 +98,7 @@ class IndexView(BulkEditMixin, ListView):
74 98
         context = super(IndexView, self).get_context_data(**kwargs)
75 99
         context['form'] = self.form
76 100
         context['queryset_description'] = self.desc_template % self.desc_ctx
101
+        context['queryset_icon'] = 'group'
77 102
         return context
78 103
 
79 104
     def make_inactive(self, request, users):

+ 1
- 1
oscar/apps/dashboard/views.py Visa fil

@@ -123,7 +123,7 @@ class IndexView(TemplateView):
123 123
 
124 124
             y_range = []
125 125
             y_axis_steps = max_value / D(str(segments))
126
-            for idx in reversed(list(range(segments + 1))):
126
+            for idx in reversed(range(segments + 1)):
127 127
                 y_range.append(idx * y_axis_steps)
128 128
         else:
129 129
             y_range = []

+ 1
- 1
oscar/apps/offer/admin.py Visa fil

@@ -12,7 +12,7 @@ class ConditionAdmin(admin.ModelAdmin):
12 12
 
13 13
 
14 14
 class BenefitAdmin(admin.ModelAdmin):
15
-    list_display = ('__unicode__', 'type', 'value', 'range')
15
+    list_display = ('__str__', 'type', 'value', 'range')
16 16
 
17 17
 
18 18
 class ConditionalOfferAdmin(admin.ModelAdmin):

+ 6
- 18
oscar/apps/offer/custom.py Visa fil

@@ -1,6 +1,5 @@
1
-import six
2
-
3 1
 from django.core import exceptions
2
+from django.db import IntegrityError
4 3
 
5 4
 from oscar.apps.offer.models import Range, Condition, Benefit
6 5
 
@@ -25,22 +24,11 @@ def create_range(range_class):
25 24
         raise exceptions.ValidationError(
26 25
             "Custom ranges must have text names (not ugettext proxies)")
27 26
 
28
-    # In Django versions further than 1.6 it will be update_or_create
29
-    # https://docs.djangoproject.com/en/dev/ref/models/querysets/#update-or-create # noqa
30
-    values = {
31
-        'name': range_class.name,
32
-        'proxy_class': _class_path(range_class),
33
-    }
34 27
     try:
35
-        obj = Range.objects.get(**values)
36
-    except Range.DoesNotExist:
37
-        obj = Range(**values)
38
-    else:
39
-        for key, value in six.iteritems(values):
40
-            setattr(obj, key, value)
41
-    obj.save()
42
-
43
-    return obj
28
+        return Range.objects.create(
29
+            name=range_class.name, proxy_class=_class_path(range_class))
30
+    except IntegrityError:
31
+        raise ValueError("The passed range already exists in the database.")
44 32
 
45 33
 
46 34
 def create_condition(condition_class):
@@ -55,7 +43,7 @@ def create_benefit(benefit_class):
55 43
     """
56 44
     Create a custom benefit instance
57 45
     """
58
-    # The custom benefit_class must override __unicode__ and description to
46
+    # The custom benefit_class must override __str__ and description to
59 47
     # avoid a recursion error
60 48
     if benefit_class.description is Benefit.description:
61 49
         raise RuntimeError("Your custom benefit must implement its own "

+ 52
- 52
oscar/apps/offer/migrations/0001_initial.py Visa fil

@@ -2,9 +2,9 @@
2 2
 from __future__ import unicode_literals
3 3
 
4 4
 from django.db import models, migrations
5
-import oscar.models.fields.autoslugfield
6
-from decimal import Decimal
7 5
 import oscar.models.fields
6
+from decimal import Decimal
7
+import oscar.models.fields.autoslugfield
8 8
 from django.conf import settings
9 9
 
10 10
 
@@ -19,11 +19,11 @@ class Migration(migrations.Migration):
19 19
         migrations.CreateModel(
20 20
             name='Benefit',
21 21
             fields=[
22
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
23
-                ('type', models.CharField(blank=True, max_length=128, verbose_name='Type', choices=[(b'Percentage', "Discount is a percentage off of the product's value"), (b'Absolute', "Discount is a fixed amount off of the product's value"), (b'Multibuy', 'Discount is to give the cheapest product for free'), (b'Fixed price', 'Get the products that meet the condition for a fixed price'), (b'Shipping absolute', 'Discount is a fixed amount of the shipping cost'), (b'Shipping fixed price', 'Get shipping for a fixed price'), (b'Shipping percentage', 'Discount is a percentage off of the shipping cost')])),
24
-                ('value', oscar.models.fields.PositiveDecimalField(null=True, verbose_name='Value', max_digits=12, decimal_places=2, blank=True)),
25
-                ('max_affected_items', models.PositiveIntegerField(help_text='Set this to prevent the discount consuming all items within the range that are in the basket.', null=True, verbose_name='Max Affected Items', blank=True)),
26
-                ('proxy_class', oscar.models.fields.NullCharField(default=None, max_length=255, unique=True, verbose_name='Custom class')),
22
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
23
+                ('type', models.CharField(verbose_name='Type', choices=[('Percentage', "Discount is a percentage off of the product's value"), ('Absolute', "Discount is a fixed amount off of the product's value"), ('Multibuy', 'Discount is to give the cheapest product for free'), ('Fixed price', 'Get the products that meet the condition for a fixed price'), ('Shipping absolute', 'Discount is a fixed amount of the shipping cost'), ('Shipping fixed price', 'Get shipping for a fixed price'), ('Shipping percentage', 'Discount is a percentage off of the shipping cost')], blank=True, max_length=128)),
24
+                ('value', oscar.models.fields.PositiveDecimalField(verbose_name='Value', max_digits=12, decimal_places=2, blank=True, null=True)),
25
+                ('max_affected_items', models.PositiveIntegerField(verbose_name='Max Affected Items', blank=True, help_text='Set this to prevent the discount consuming all items within the range that are in the basket.', null=True)),
26
+                ('proxy_class', oscar.models.fields.NullCharField(verbose_name='Custom class', default=None, max_length=255, unique=True)),
27 27
             ],
28 28
             options={
29 29
                 'verbose_name': 'Benefit',
@@ -34,10 +34,10 @@ class Migration(migrations.Migration):
34 34
         migrations.CreateModel(
35 35
             name='Condition',
36 36
             fields=[
37
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
38
-                ('type', models.CharField(blank=True, max_length=128, verbose_name='Type', choices=[(b'Count', 'Depends on number of items in basket that are in condition range'), (b'Value', 'Depends on value of items in basket that are in condition range'), (b'Coverage', 'Needs to contain a set number of DISTINCT items from the condition range')])),
39
-                ('value', oscar.models.fields.PositiveDecimalField(null=True, verbose_name='Value', max_digits=12, decimal_places=2, blank=True)),
40
-                ('proxy_class', oscar.models.fields.NullCharField(default=None, max_length=255, unique=True, verbose_name='Custom class')),
37
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
38
+                ('type', models.CharField(verbose_name='Type', choices=[('Count', 'Depends on number of items in basket that are in condition range'), ('Value', 'Depends on value of items in basket that are in condition range'), ('Coverage', 'Needs to contain a set number of DISTINCT items from the condition range')], blank=True, max_length=128)),
39
+                ('value', oscar.models.fields.PositiveDecimalField(verbose_name='Value', max_digits=12, decimal_places=2, blank=True, null=True)),
40
+                ('proxy_class', oscar.models.fields.NullCharField(verbose_name='Custom class', default=None, max_length=255, unique=True)),
41 41
             ],
42 42
             options={
43 43
                 'verbose_name': 'Condition',
@@ -48,48 +48,48 @@ class Migration(migrations.Migration):
48 48
         migrations.CreateModel(
49 49
             name='ConditionalOffer',
50 50
             fields=[
51
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
52
-                ('name', models.CharField(help_text="This is displayed within the customer's basket", unique=True, max_length=128, verbose_name='Name')),
53
-                ('slug', oscar.models.fields.autoslugfield.AutoSlugField(populate_from=b'name', editable=False, max_length=128, blank=True, unique=True, verbose_name='Slug')),
54
-                ('description', models.TextField(help_text='This is displayed on the offer browsing page', verbose_name='Description', blank=True)),
55
-                ('offer_type', models.CharField(default=b'Site', max_length=128, verbose_name='Type', choices=[(b'Site', 'Site offer - available to all users'), (b'Voucher', 'Voucher offer - only available after entering the appropriate voucher code'), (b'User', 'User offer - available to certain types of user'), (b'Session', 'Session offer - temporary offer, available for a user for the duration of their session')])),
56
-                ('status', models.CharField(default=b'Open', max_length=64, verbose_name='Status')),
57
-                ('priority', models.IntegerField(default=0, help_text='The highest priority offers are applied first', verbose_name='Priority')),
58
-                ('start_datetime', models.DateTimeField(null=True, verbose_name='Start date', blank=True)),
59
-                ('end_datetime', models.DateTimeField(help_text="Offers are active until the end of the 'end date'", null=True, verbose_name='End date', blank=True)),
60
-                ('max_global_applications', models.PositiveIntegerField(help_text='The number of times this offer can be used before it is unavailable', null=True, verbose_name='Max global applications', blank=True)),
61
-                ('max_user_applications', models.PositiveIntegerField(help_text='The number of times a single user can use this offer', null=True, verbose_name='Max user applications', blank=True)),
62
-                ('max_basket_applications', models.PositiveIntegerField(help_text='The number of times this offer can be applied to a basket (and order)', null=True, verbose_name='Max basket applications', blank=True)),
63
-                ('max_discount', models.DecimalField(decimal_places=2, max_digits=12, blank=True, help_text='When an offer has given more discount to orders than this threshold, then the offer becomes unavailable', null=True, verbose_name='Max discount')),
64
-                ('total_discount', models.DecimalField(default=Decimal('0.00'), verbose_name='Total Discount', max_digits=12, decimal_places=2)),
65
-                ('num_applications', models.PositiveIntegerField(default=0, verbose_name='Number of applications')),
66
-                ('num_orders', models.PositiveIntegerField(default=0, verbose_name='Number of Orders')),
51
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
52
+                ('name', models.CharField(verbose_name='Name', unique=True, help_text="This is displayed within the customer's basket", max_length=128)),
53
+                ('slug', oscar.models.fields.autoslugfield.AutoSlugField(editable=False, verbose_name='Slug', blank=True, max_length=128, populate_from='name', unique=True)),
54
+                ('description', models.TextField(verbose_name='Description', blank=True, help_text='This is displayed on the offer browsing page')),
55
+                ('offer_type', models.CharField(verbose_name='Type', choices=[('Site', 'Site offer - available to all users'), ('Voucher', 'Voucher offer - only available after entering the appropriate voucher code'), ('User', 'User offer - available to certain types of user'), ('Session', 'Session offer - temporary offer, available for a user for the duration of their session')], default='Site', max_length=128)),
56
+                ('status', models.CharField(verbose_name='Status', default='Open', max_length=64)),
57
+                ('priority', models.IntegerField(verbose_name='Priority', help_text='The highest priority offers are applied first', default=0)),
58
+                ('start_datetime', models.DateTimeField(verbose_name='Start date', blank=True, null=True)),
59
+                ('end_datetime', models.DateTimeField(verbose_name='End date', blank=True, help_text="Offers are active until the end of the 'end date'", null=True)),
60
+                ('max_global_applications', models.PositiveIntegerField(verbose_name='Max global applications', blank=True, help_text='The number of times this offer can be used before it is unavailable', null=True)),
61
+                ('max_user_applications', models.PositiveIntegerField(verbose_name='Max user applications', blank=True, help_text='The number of times a single user can use this offer', null=True)),
62
+                ('max_basket_applications', models.PositiveIntegerField(verbose_name='Max basket applications', blank=True, help_text='The number of times this offer can be applied to a basket (and order)', null=True)),
63
+                ('max_discount', models.DecimalField(verbose_name='Max discount', decimal_places=2, blank=True, max_digits=12, help_text='When an offer has given more discount to orders than this threshold, then the offer becomes unavailable', null=True)),
64
+                ('total_discount', models.DecimalField(verbose_name='Total Discount', max_digits=12, decimal_places=2, default=Decimal('0.00'))),
65
+                ('num_applications', models.PositiveIntegerField(verbose_name='Number of applications', default=0)),
66
+                ('num_orders', models.PositiveIntegerField(verbose_name='Number of Orders', default=0)),
67 67
                 ('redirect_url', oscar.models.fields.ExtendedURLField(verbose_name='URL redirect (optional)', blank=True)),
68
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
68
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
69 69
                 ('benefit', models.ForeignKey(verbose_name='Benefit', to='offer.Benefit')),
70 70
                 ('condition', models.ForeignKey(verbose_name='Condition', to='offer.Condition')),
71 71
             ],
72 72
             options={
73
-                'ordering': [b'-priority'],
74 73
                 'verbose_name': 'Conditional offer',
75 74
                 'verbose_name_plural': 'Conditional offers',
75
+                'ordering': ['-priority'],
76 76
             },
77 77
             bases=(models.Model,),
78 78
         ),
79 79
         migrations.CreateModel(
80 80
             name='Range',
81 81
             fields=[
82
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
83
-                ('name', models.CharField(unique=True, max_length=128, verbose_name='Name')),
84
-                ('slug', oscar.models.fields.autoslugfield.AutoSlugField(populate_from=b'name', editable=False, max_length=128, blank=True, unique=True, verbose_name='Slug')),
82
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
83
+                ('name', models.CharField(verbose_name='Name', unique=True, max_length=128)),
84
+                ('slug', oscar.models.fields.autoslugfield.AutoSlugField(editable=False, verbose_name='Slug', blank=True, max_length=128, populate_from='name', unique=True)),
85 85
                 ('description', models.TextField(blank=True)),
86
-                ('is_public', models.BooleanField(default=False, help_text='Public ranges have a customer-facing page', verbose_name='Is public?')),
87
-                ('includes_all_products', models.BooleanField(default=False, verbose_name='Includes all products?')),
88
-                ('proxy_class', oscar.models.fields.NullCharField(default=None, max_length=255, unique=True, verbose_name='Custom class')),
89
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
90
-                ('classes', models.ManyToManyField(to='catalogue.ProductClass', verbose_name='Product Types', blank=True)),
91
-                ('excluded_products', models.ManyToManyField(to='catalogue.Product', verbose_name='Excluded Products', blank=True)),
92
-                ('included_categories', models.ManyToManyField(to='catalogue.Category', verbose_name='Included Categories', blank=True)),
86
+                ('is_public', models.BooleanField(verbose_name='Is public?', help_text='Public ranges have a customer-facing page', default=False)),
87
+                ('includes_all_products', models.BooleanField(verbose_name='Includes all products?', default=False)),
88
+                ('proxy_class', oscar.models.fields.NullCharField(verbose_name='Custom class', default=None, max_length=255, unique=True)),
89
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
90
+                ('classes', models.ManyToManyField(verbose_name='Product Types', blank=True, to='catalogue.ProductClass')),
91
+                ('excluded_products', models.ManyToManyField(verbose_name='Excluded Products', blank=True, to='catalogue.Product')),
92
+                ('included_categories', models.ManyToManyField(verbose_name='Included Categories', blank=True, to='catalogue.Category')),
93 93
             ],
94 94
             options={
95 95
                 'verbose_name': 'Range',
@@ -112,7 +112,7 @@ class Migration(migrations.Migration):
112 112
         migrations.CreateModel(
113 113
             name='RangeProduct',
114 114
             fields=[
115
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
115
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
116 116
                 ('display_order', models.IntegerField(default=0)),
117 117
                 ('product', models.ForeignKey(to='catalogue.Product')),
118 118
             ],
@@ -123,7 +123,7 @@ class Migration(migrations.Migration):
123 123
         migrations.AddField(
124 124
             model_name='range',
125 125
             name='included_products',
126
-            field=models.ManyToManyField(to='catalogue.Product', verbose_name='Included Products', through='offer.RangeProduct', blank=True),
126
+            field=models.ManyToManyField(verbose_name='Included Products', through='offer.RangeProduct', blank=True, to='catalogue.Product'),
127 127
             preserve_default=True,
128 128
         ),
129 129
         migrations.AddField(
@@ -134,28 +134,28 @@ class Migration(migrations.Migration):
134 134
         ),
135 135
         migrations.AlterUniqueTogether(
136 136
             name='rangeproduct',
137
-            unique_together=set([(b'range', b'product')]),
137
+            unique_together=set([('range', 'product')]),
138 138
         ),
139 139
         migrations.CreateModel(
140 140
             name='RangeProductFileUpload',
141 141
             fields=[
142
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
143
-                ('filepath', models.CharField(max_length=255, verbose_name='File Path')),
142
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
143
+                ('filepath', models.CharField(verbose_name='File Path', max_length=255)),
144 144
                 ('size', models.PositiveIntegerField(verbose_name='Size')),
145
-                ('date_uploaded', models.DateTimeField(auto_now_add=True, verbose_name='Date Uploaded')),
146
-                ('status', models.CharField(default=b'Pending', max_length=32, verbose_name='Status', choices=[(b'Pending', b'Pending'), (b'Failed', b'Failed'), (b'Processed', b'Processed')])),
147
-                ('error_message', models.CharField(max_length=255, verbose_name='Error Message', blank=True)),
148
-                ('date_processed', models.DateTimeField(null=True, verbose_name='Date Processed')),
149
-                ('num_new_skus', models.PositiveIntegerField(null=True, verbose_name='Number of New SKUs')),
150
-                ('num_unknown_skus', models.PositiveIntegerField(null=True, verbose_name='Number of Unknown SKUs')),
151
-                ('num_duplicate_skus', models.PositiveIntegerField(null=True, verbose_name='Number of Duplicate SKUs')),
145
+                ('date_uploaded', models.DateTimeField(verbose_name='Date Uploaded', auto_now_add=True)),
146
+                ('status', models.CharField(verbose_name='Status', choices=[('Pending', 'Pending'), ('Failed', 'Failed'), ('Processed', 'Processed')], default='Pending', max_length=32)),
147
+                ('error_message', models.CharField(verbose_name='Error Message', blank=True, max_length=255)),
148
+                ('date_processed', models.DateTimeField(verbose_name='Date Processed', null=True)),
149
+                ('num_new_skus', models.PositiveIntegerField(verbose_name='Number of New SKUs', null=True)),
150
+                ('num_unknown_skus', models.PositiveIntegerField(verbose_name='Number of Unknown SKUs', null=True)),
151
+                ('num_duplicate_skus', models.PositiveIntegerField(verbose_name='Number of Duplicate SKUs', null=True)),
152 152
                 ('range', models.ForeignKey(verbose_name='Range', to='offer.Range')),
153 153
                 ('uploaded_by', models.ForeignKey(verbose_name='Uploaded By', to=settings.AUTH_USER_MODEL)),
154 154
             ],
155 155
             options={
156
-                'ordering': (b'-date_uploaded',),
157 156
                 'verbose_name': 'Range Product Uploaded File',
158 157
                 'verbose_name_plural': 'Range Product Uploaded Files',
158
+                'ordering': ('-date_uploaded',),
159 159
             },
160 160
             bases=(models.Model,),
161 161
         ),

+ 66
- 46
oscar/apps/offer/models.py Visa fil

@@ -1,21 +1,22 @@
1
+import itertools
1 2
 import os
2 3
 import re
3
-import six
4 4
 import operator
5 5
 from decimal import Decimal as D, ROUND_DOWN, ROUND_UP
6 6
 
7 7
 from django.core import exceptions
8 8
 from django.template.defaultfilters import date as date_filter
9 9
 from django.db import models
10
+from django.utils.encoding import python_2_unicode_compatible
10 11
 from django.utils.timezone import now, get_current_timezone
11 12
 from django.utils.translation import ungettext, ugettext_lazy as _
12 13
 from django.utils.importlib import import_module
14
+from django.utils import six
13 15
 from django.core.exceptions import ValidationError
14 16
 from django.core.urlresolvers import reverse
15 17
 from django.conf import settings
16 18
 
17 19
 from oscar.core.compat import AUTH_USER_MODEL
18
-from oscar.core.utils import slugify
19 20
 from oscar.core.loading import get_class, get_model
20 21
 from oscar.apps.offer.managers import ActiveOfferManager
21 22
 from oscar.templatetags.currency_filters import currency
@@ -60,6 +61,7 @@ def apply_discount(line, discount, quantity):
60 61
     line.discount(discount, quantity, incl_tax=False)
61 62
 
62 63
 
64
+@python_2_unicode_compatible
63 65
 class ConditionalOffer(models.Model):
64 66
     """
65 67
     A conditional offer (eg buy 1, get 10% off)
@@ -193,7 +195,7 @@ class ConditionalOffer(models.Model):
193 195
     def get_absolute_url(self):
194 196
         return reverse('offer:detail', kwargs={'slug': self.slug})
195 197
 
196
-    def __unicode__(self):
198
+    def __str__(self):
197 199
         return self.name
198 200
 
199 201
     def clean(self):
@@ -402,15 +404,19 @@ class ConditionalOffer(models.Model):
402 404
         cond_range = self.condition.range
403 405
         if cond_range.includes_all_products:
404 406
             # Return ALL the products
405
-            return Product.browsable.select_related('product_class',
406
-                                                    'stockrecord')\
407
-                .filter(is_discountable=True)\
408
-                .prefetch_related('children', 'images',
409
-                                  'product_class__options', 'product_options')
410
-        return cond_range.included_products.filter(is_discountable=True)
407
+            queryset = Product.browsable
408
+        else:
409
+            queryset = cond_range.included_products
410
+        return queryset.filter(is_discountable=True).exclude(
411
+            structure=Product.CHILD)
411 412
 
412 413
 
414
+@python_2_unicode_compatible
413 415
 class Condition(models.Model):
416
+    """
417
+    A condition for an offer to be applied. You can either specify a custom
418
+    proxy class, or need to specify a type, range and value.
419
+    """
414 420
     COUNT, VALUE, COVERAGE = ("Count", "Value", "Coverage")
415 421
     TYPE_CHOICES = (
416 422
         (COUNT, _("Depends on number of items in basket that are in "
@@ -452,9 +458,9 @@ class Condition(models.Model):
452 458
             self.COVERAGE: CoverageCondition}
453 459
         if self.type in klassmap:
454 460
             return klassmap[self.type](**field_dict)
455
-        return self
461
+        raise RuntimeError("Unrecognised condition type (%s)" % self.type)
456 462
 
457
-    def __unicode__(self):
463
+    def __str__(self):
458 464
         return self.proxy().name
459 465
 
460 466
     @property
@@ -524,6 +530,7 @@ class Condition(models.Model):
524 530
         return sorted(line_tuples, key=key)
525 531
 
526 532
 
533
+@python_2_unicode_compatible
527 534
 class Benefit(models.Model):
528 535
     range = models.ForeignKey(
529 536
         'offer.Range', null=True, blank=True, verbose_name=_("Range"))
@@ -592,7 +599,7 @@ class Benefit(models.Model):
592 599
             return klassmap[self.type](**field_dict)
593 600
         raise RuntimeError("Unrecognised benefit type (%s)" % self.type)
594 601
 
595
-    def __unicode__(self):
602
+    def __str__(self):
596 603
         name = self.proxy().name
597 604
         if self.max_affected_items:
598 605
             name += ungettext(
@@ -746,9 +753,13 @@ class Benefit(models.Model):
746 753
         return D('0.00')
747 754
 
748 755
 
756
+@python_2_unicode_compatible
749 757
 class Range(models.Model):
750 758
     """
751
-    Represents a range of products that can be used within an offer
759
+    Represents a range of products that can be used within an offer.
760
+
761
+    Ranges only support adding parent or stand-alone products. Offers will
762
+    consider child products automatically.
752 763
     """
753 764
     name = models.CharField(_("Name"), max_length=128, unique=True)
754 765
     slug = fields.AutoSlugField(
@@ -795,15 +806,12 @@ class Range(models.Model):
795 806
         verbose_name = _("Range")
796 807
         verbose_name_plural = _("Ranges")
797 808
 
798
-    def __unicode__(self):
809
+    def __str__(self):
799 810
         return self.name
800 811
 
801 812
     def get_absolute_url(self):
802
-        return reverse('catalogue:range', kwargs={
803
-            'slug': self.slug})
804
-
805
-    def _save(self, *args, **kwargs):
806
-        super(Range, self).save(*args, **kwargs)
813
+        return reverse(
814
+            'catalogue:range', kwargs={'slug': self.slug})
807 815
 
808 816
     def add_product(self, product, display_order=None):
809 817
         """ Add product to the range
@@ -811,18 +819,14 @@ class Range(models.Model):
811 819
         When adding product that is already in the range, prevent re-adding it.
812 820
         If display_order is specified, update it.
813 821
 
814
-        Standard display_order for a new product in the range (0) puts
822
+        Default display_order for a new product in the range is 0; this puts
815 823
         the product at the top of the list.
816
-
817
-        display_order needs to be tested for None because
818
-
819
-          >>> display_order = 0
820
-          >>> not display_order
821
-          True
822
-          >>> display_order is None
823
-          False
824 824
         """
825
-        initial_order = 0 if display_order is None else display_order
825
+        if product.is_child:
826
+            raise ValueError(
827
+                "Ranges can only contain parent and stand-alone products.")
828
+
829
+        initial_order = display_order or 0
826 830
         relation, __ = RangeProduct.objects.get_or_create(
827 831
             range=self, product=product,
828 832
             defaults={'display_order': initial_order})
@@ -833,19 +837,19 @@ class Range(models.Model):
833 837
             relation.save()
834 838
 
835 839
     def remove_product(self, product):
836
-        """ Remove product from range """
840
+        """
841
+        Remove product from range. To save on queries, this function does not
842
+        check if the product is in fact in the range.
843
+        """
837 844
         RangeProduct.objects.filter(range=self, product=product).delete()
838 845
 
839 846
     def contains_product(self, product):  # noqa (too complex (12))
840 847
         """
841
-        Check whether the passed product is part of this range
848
+        Check whether the passed product is part of this range.
842 849
         """
843
-        # We look for shortcircuit checks first before
844
-        # the tests that require more database queries.
845
-
846
-        if settings.OSCAR_OFFER_BLACKLIST_PRODUCT and \
847
-                settings.OSCAR_OFFER_BLACKLIST_PRODUCT(product):
848
-            return False
850
+        # Child products are never part of the range, but the parent may be.
851
+        if product.is_child:
852
+            product = product.parent
849 853
 
850 854
         # Delegate to a proxy class if one is provided
851 855
         if self.proxy_class:
@@ -863,7 +867,7 @@ class Range(models.Model):
863 867
             return True
864 868
         test_categories = self.included_categories.all()
865 869
         if test_categories:
866
-            for category in product.categories.all():
870
+            for category in product.get_categories().all():
867 871
                 for test_category in test_categories:
868 872
                     if category == test_category \
869 873
                             or category.is_descendant_of(test_category):
@@ -873,23 +877,39 @@ class Range(models.Model):
873 877
     # Shorter alias
874 878
     contains = contains_product
875 879
 
880
+    def __get_pks_and_child_pks(self, queryset):
881
+        """
882
+        Expects a product queryset; gets the primary keys of the passed
883
+        products and their children.
884
+
885
+        Verbose, but database and memory friendly.
886
+        """
887
+        # One query to get parent and children; [(4, None), (5, 10), (5, 11)]
888
+        pk_tuples_iterable = queryset.values_list('pk', 'children__pk')
889
+        # Flatten list without unpacking; [4, None, 5, 10, 5, 11]
890
+        flat_iterable = itertools.chain.from_iterable(pk_tuples_iterable)
891
+        # Ensure uniqueness and remove None; {4, 5, 10, 11}
892
+        return set(flat_iterable) - {None}
893
+
876 894
     def _included_product_ids(self):
895
+        if not self.id:
896
+            return []
877 897
         if self.__included_product_ids is None:
878
-            self.__included_product_ids = [row['id'] for row in
879
-                                           self.included_products.values('id')]
898
+            self.__included_product_ids = self.__get_pks_and_child_pks(
899
+                self.included_products)
880 900
         return self.__included_product_ids
881 901
 
882 902
     def _excluded_product_ids(self):
883 903
         if not self.id:
884 904
             return []
885 905
         if self.__excluded_product_ids is None:
886
-            self.__excluded_product_ids = [row['id'] for row in
887
-                                           self.excluded_products.values('id')]
906
+            self.__excluded_product_ids = self.__get_pks_and_child_pks(
907
+                self.excluded_products)
888 908
         return self.__excluded_product_ids
889 909
 
890 910
     def _class_ids(self):
891 911
         if None == self.__class_ids:
892
-            self.__class_ids = [row['id'] for row in self.classes.values('id')]
912
+            self.__class_ids = self.classes.values_list('pk', flat=True)
893 913
         return self.__class_ids
894 914
 
895 915
     def num_products(self):
@@ -1218,7 +1238,7 @@ class ApplicationResult(object):
1218 1238
     # (a) Give a discount off the BASKET total
1219 1239
     # (b) Give a discount off the SHIPPING total
1220 1240
     # (a) Trigger a post-order action
1221
-    BASKET, SHIPPING, POST_ORDER = list(range(0, 3))
1241
+    BASKET, SHIPPING, POST_ORDER = 0, 1, 2
1222 1242
     affects = None
1223 1243
 
1224 1244
     @property
@@ -1423,13 +1443,13 @@ class FixedPriceBenefit(Benefit):
1423 1443
     _description = _("The products that meet the condition are sold "
1424 1444
                      "for %(amount)s")
1425 1445
 
1426
-    def __unicode__(self):
1446
+    def __str__(self):
1427 1447
         return self._description % {
1428 1448
             'amount': currency(self.value)}
1429 1449
 
1430 1450
     @property
1431 1451
     def description(self):
1432
-        return self.__unicode__()
1452
+        return six.text_type(self)
1433 1453
 
1434 1454
     class Meta:
1435 1455
         proxy = True

+ 30
- 15
oscar/apps/order/abstract_models.py Visa fil

@@ -6,6 +6,7 @@ from django.conf import settings
6 6
 from django.db import models
7 7
 from django.db.models import Sum
8 8
 from django.utils import timezone
9
+from django.utils.encoding import python_2_unicode_compatible
9 10
 from django.utils.translation import ugettext_lazy as _, pgettext_lazy
10 11
 from django.utils.datastructures import SortedDict
11 12
 
@@ -14,6 +15,7 @@ from oscar.models.fields import AutoSlugField
14 15
 from . import exceptions
15 16
 
16 17
 
18
+@python_2_unicode_compatible
17 19
 class AbstractOrder(models.Model):
18 20
     """
19 21
     The main order model
@@ -95,7 +97,7 @@ class AbstractOrder(models.Model):
95 97
         """
96 98
         Return all possible statuses for an order
97 99
         """
98
-        return cls.pipeline.keys()
100
+        return list(cls.pipeline.keys())
99 101
 
100 102
     def available_statuses(self):
101 103
         """
@@ -272,7 +274,7 @@ class AbstractOrder(models.Model):
272 274
         verbose_name = _("Order")
273 275
         verbose_name_plural = _("Orders")
274 276
 
275
-    def __unicode__(self):
277
+    def __str__(self):
276 278
         return u"#%s" % (self.number,)
277 279
 
278 280
     def verification_hash(self):
@@ -304,6 +306,7 @@ class AbstractOrder(models.Model):
304 306
             category=AbstractOrderDiscount.DEFERRED)
305 307
 
306 308
 
309
+@python_2_unicode_compatible
307 310
 class AbstractOrderNote(models.Model):
308 311
     """
309 312
     A note against an order.
@@ -336,7 +339,7 @@ class AbstractOrderNote(models.Model):
336 339
         verbose_name = _("Order Note")
337 340
         verbose_name_plural = _("Order Notes")
338 341
 
339
-    def __unicode__(self):
342
+    def __str__(self):
340 343
         return u"'%s' (%s)" % (self.message[0:50], self.user)
341 344
 
342 345
     def is_editable(self):
@@ -346,6 +349,7 @@ class AbstractOrderNote(models.Model):
346 349
         return delta.seconds < self.editable_lifetime
347 350
 
348 351
 
352
+@python_2_unicode_compatible
349 353
 class AbstractCommunicationEvent(models.Model):
350 354
     """
351 355
     An order-level event involving a communication to the customer, such
@@ -365,7 +369,7 @@ class AbstractCommunicationEvent(models.Model):
365 369
         verbose_name_plural = _("Communication Events")
366 370
         ordering = ['-date_created']
367 371
 
368
-    def __unicode__(self):
372
+    def __str__(self):
369 373
         return _("'%(type)s' event for order #%(number)s") \
370 374
             % {'type': self.event_type.name, 'number': self.order.number}
371 375
 
@@ -373,6 +377,7 @@ class AbstractCommunicationEvent(models.Model):
373 377
 # LINES
374 378
 
375 379
 
380
+@python_2_unicode_compatible
376 381
 class AbstractLine(models.Model):
377 382
     """
378 383
     An order line
@@ -479,7 +484,7 @@ class AbstractLine(models.Model):
479 484
         verbose_name = _("Order Line")
480 485
         verbose_name_plural = _("Order Lines")
481 486
 
482
-    def __unicode__(self):
487
+    def __str__(self):
483 488
         if self.product:
484 489
             title = self.product.title
485 490
         else:
@@ -492,7 +497,7 @@ class AbstractLine(models.Model):
492 497
         """
493 498
         Return all possible statuses for an order line
494 499
         """
495
-        return cls.pipeline.keys()
500
+        return list(cls.pipeline.keys())
496 501
 
497 502
     def available_statuses(self):
498 503
         """
@@ -641,7 +646,9 @@ class AbstractLine(models.Model):
641 646
 
642 647
     def is_payment_event_permitted(self, event_type, quantity):
643 648
         """
644
-        Test whether a payment event with the given quantity is permitted
649
+        Test whether a payment event with the given quantity is permitted.
650
+
651
+        Allow each payment event type to occur only once per quantity.
645 652
         """
646 653
         current_qty = self.payment_event_quantity(event_type)
647 654
         return (current_qty + quantity) <= self.quantity
@@ -686,6 +693,7 @@ class AbstractLine(models.Model):
686 693
         return True, None
687 694
 
688 695
 
696
+@python_2_unicode_compatible
689 697
 class AbstractLineAttribute(models.Model):
690 698
     """
691 699
     An attribute of a line
@@ -705,10 +713,11 @@ class AbstractLineAttribute(models.Model):
705 713
         verbose_name = _("Line Attribute")
706 714
         verbose_name_plural = _("Line Attributes")
707 715
 
708
-    def __unicode__(self):
716
+    def __str__(self):
709 717
         return "%s = %s" % (self.type, self.value)
710 718
 
711 719
 
720
+@python_2_unicode_compatible
712 721
 class AbstractLinePrice(models.Model):
713 722
     """
714 723
     For tracking the prices paid for each unit within a line.
@@ -738,7 +747,7 @@ class AbstractLinePrice(models.Model):
738 747
         verbose_name = _("Line Price")
739 748
         verbose_name_plural = _("Line Prices")
740 749
 
741
-    def __unicode__(self):
750
+    def __str__(self):
742 751
         return _("Line '%(number)s' (quantity %(qty)d) price %(price)s") % {
743 752
             'number': self.line,
744 753
             'qty': self.quantity,
@@ -748,6 +757,7 @@ class AbstractLinePrice(models.Model):
748 757
 # PAYMENT EVENTS
749 758
 
750 759
 
760
+@python_2_unicode_compatible
751 761
 class AbstractPaymentEventType(models.Model):
752 762
     """
753 763
     Payment event types are things like 'Paid', 'Failed', 'Refunded'.
@@ -765,10 +775,11 @@ class AbstractPaymentEventType(models.Model):
765 775
         verbose_name_plural = _("Payment Event Types")
766 776
         ordering = ('name', )
767 777
 
768
-    def __unicode__(self):
778
+    def __str__(self):
769 779
         return self.name
770 780
 
771 781
 
782
+@python_2_unicode_compatible
772 783
 class AbstractPaymentEvent(models.Model):
773 784
     """
774 785
     A payment event for an order
@@ -807,7 +818,7 @@ class AbstractPaymentEvent(models.Model):
807 818
         verbose_name_plural = _("Payment Events")
808 819
         ordering = ['-date_created']
809 820
 
810
-    def __unicode__(self):
821
+    def __str__(self):
811 822
         return _("Payment event for order %s") % self.order
812 823
 
813 824
     def num_affected_lines(self):
@@ -836,6 +847,7 @@ class PaymentEventQuantity(models.Model):
836 847
 # SHIPPING EVENTS
837 848
 
838 849
 
850
+@python_2_unicode_compatible
839 851
 class AbstractShippingEvent(models.Model):
840 852
     """
841 853
     An event is something which happens to a group of lines such as
@@ -861,7 +873,7 @@ class AbstractShippingEvent(models.Model):
861 873
         verbose_name_plural = _("Shipping Events")
862 874
         ordering = ['-date_created']
863 875
 
864
-    def __unicode__(self):
876
+    def __str__(self):
865 877
         return _("Order #%(number)s, type %(type)s") % {
866 878
             'number': self.order.number,
867 879
             'type': self.event_type}
@@ -870,6 +882,7 @@ class AbstractShippingEvent(models.Model):
870 882
         return self.lines.count()
871 883
 
872 884
 
885
+@python_2_unicode_compatible
873 886
 class ShippingEventQuantity(models.Model):
874 887
     """
875 888
     A "through" model linking lines to shipping events.
@@ -901,12 +914,13 @@ class ShippingEventQuantity(models.Model):
901 914
             raise exceptions.InvalidShippingEvent
902 915
         super(ShippingEventQuantity, self).save(*args, **kwargs)
903 916
 
904
-    def __unicode__(self):
917
+    def __str__(self):
905 918
         return _("%(product)s - quantity %(qty)d") % {
906 919
             'product': self.line.product,
907 920
             'qty': self.quantity}
908 921
 
909 922
 
923
+@python_2_unicode_compatible
910 924
 class AbstractShippingEventType(models.Model):
911 925
     """
912 926
     A type of shipping/fulfillment event
@@ -926,13 +940,14 @@ class AbstractShippingEventType(models.Model):
926 940
         verbose_name_plural = _("Shipping Event Types")
927 941
         ordering = ('name', )
928 942
 
929
-    def __unicode__(self):
943
+    def __str__(self):
930 944
         return self.name
931 945
 
932 946
 
933 947
 # DISCOUNTS
934 948
 
935 949
 
950
+@python_2_unicode_compatible
936 951
 class AbstractOrderDiscount(models.Model):
937 952
     """
938 953
     A discount against an order.
@@ -1006,7 +1021,7 @@ class AbstractOrderDiscount(models.Model):
1006 1021
 
1007 1022
         super(AbstractOrderDiscount, self).save(**kwargs)
1008 1023
 
1009
-    def __unicode__(self):
1024
+    def __str__(self):
1010 1025
         return _("Discount of %(amount)r from order %(order)s") % {
1011 1026
             'amount': self.amount, 'order': self.order}
1012 1027
 

+ 1
- 10
oscar/apps/order/admin.py Visa fil

@@ -58,15 +58,6 @@ class PaymentEventTypeAdmin(admin.ModelAdmin):
58 58
     pass
59 59
 
60 60
 
61
-class OrderNoteAdmin(admin.ModelAdmin):
62
-    exclude = ('user',)
63
-
64
-    def save_model(self, request, obj, form, change):
65
-        if not change:
66
-            obj.user = request.user
67
-        obj.save()
68
-
69
-
70 61
 class OrderDiscountAdmin(admin.ModelAdmin):
71 62
     readonly_fields = ('order', 'category', 'offer_id', 'offer_name',
72 63
                        'voucher_id', 'voucher_code', 'amount')
@@ -75,7 +66,7 @@ class OrderDiscountAdmin(admin.ModelAdmin):
75 66
 
76 67
 
77 68
 admin.site.register(Order, OrderAdmin)
78
-admin.site.register(OrderNote, OrderNoteAdmin)
69
+admin.site.register(OrderNote)
79 70
 admin.site.register(ShippingAddress)
80 71
 admin.site.register(Line, LineAdmin)
81 72
 admin.site.register(LinePrice, LinePriceAdmin)

+ 119
- 119
oscar/apps/order/migrations/0001_initial.py Visa fil

@@ -2,8 +2,8 @@
2 2
 from __future__ import unicode_literals
3 3
 
4 4
 from django.db import models, migrations
5
-import oscar.models.fields.autoslugfield
6 5
 import oscar.models.fields
6
+import oscar.models.fields.autoslugfield
7 7
 import django.db.models.deletion
8 8
 from django.conf import settings
9 9
 
@@ -12,145 +12,145 @@ class Migration(migrations.Migration):
12 12
 
13 13
     dependencies = [
14 14
         migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15
-        ('address', '0001_initial'),
16
-        ('sites', '__latest__'),
17 15
         ('partner', '0001_initial'),
18
-        ('customer', '0001_initial'),
19
-        ('catalogue', '0001_initial'),
20 16
         ('basket', '0001_initial'),
17
+        ('catalogue', '0001_initial'),
18
+        ('customer', '0001_initial'),
19
+        ('sites', '0001_initial'),
20
+        ('address', '0001_initial'),
21 21
     ]
22 22
 
23 23
     operations = [
24 24
         migrations.CreateModel(
25 25
             name='BillingAddress',
26 26
             fields=[
27
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
28
-                ('title', models.CharField(blank=True, max_length=64, verbose_name='Title', choices=[(b'Mr', 'Mr'), (b'Miss', 'Miss'), (b'Mrs', 'Mrs'), (b'Ms', 'Ms'), (b'Dr', 'Dr')])),
29
-                ('first_name', models.CharField(max_length=255, verbose_name='First name', blank=True)),
30
-                ('last_name', models.CharField(max_length=255, verbose_name='Last name', blank=True)),
31
-                ('line1', models.CharField(max_length=255, verbose_name='First line of address')),
32
-                ('line2', models.CharField(max_length=255, verbose_name='Second line of address', blank=True)),
33
-                ('line3', models.CharField(max_length=255, verbose_name='Third line of address', blank=True)),
34
-                ('line4', models.CharField(max_length=255, verbose_name='City', blank=True)),
35
-                ('state', models.CharField(max_length=255, verbose_name='State/County', blank=True)),
36
-                ('postcode', oscar.models.fields.UppercaseCharField(max_length=64, verbose_name='Post/Zip-code', blank=True)),
37
-                ('search_text', models.TextField(verbose_name='Search text - used only for searching addresses', editable=False)),
27
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
28
+                ('title', models.CharField(verbose_name='Title', choices=[('Mr', 'Mr'), ('Miss', 'Miss'), ('Mrs', 'Mrs'), ('Ms', 'Ms'), ('Dr', 'Dr')], blank=True, max_length=64)),
29
+                ('first_name', models.CharField(verbose_name='First name', blank=True, max_length=255)),
30
+                ('last_name', models.CharField(verbose_name='Last name', blank=True, max_length=255)),
31
+                ('line1', models.CharField(verbose_name='First line of address', max_length=255)),
32
+                ('line2', models.CharField(verbose_name='Second line of address', blank=True, max_length=255)),
33
+                ('line3', models.CharField(verbose_name='Third line of address', blank=True, max_length=255)),
34
+                ('line4', models.CharField(verbose_name='City', blank=True, max_length=255)),
35
+                ('state', models.CharField(verbose_name='State/County', blank=True, max_length=255)),
36
+                ('postcode', oscar.models.fields.UppercaseCharField(verbose_name='Post/Zip-code', blank=True, max_length=64)),
37
+                ('search_text', models.TextField(editable=False, verbose_name='Search text - used only for searching addresses')),
38 38
                 ('country', models.ForeignKey(verbose_name='Country', to='address.Country')),
39 39
             ],
40 40
             options={
41
-                'abstract': False,
42 41
                 'verbose_name': 'Billing address',
43 42
                 'verbose_name_plural': 'Billing addresses',
43
+                'abstract': False,
44 44
             },
45 45
             bases=(models.Model,),
46 46
         ),
47 47
         migrations.CreateModel(
48 48
             name='CommunicationEvent',
49 49
             fields=[
50
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
51
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date')),
50
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
51
+                ('date_created', models.DateTimeField(verbose_name='Date', auto_now_add=True)),
52 52
                 ('event_type', models.ForeignKey(verbose_name='Event Type', to='customer.CommunicationEventType')),
53 53
             ],
54 54
             options={
55
-                'ordering': [b'-date_created'],
56
-                'abstract': False,
57 55
                 'verbose_name': 'Communication Event',
58 56
                 'verbose_name_plural': 'Communication Events',
57
+                'ordering': ['-date_created'],
58
+                'abstract': False,
59 59
             },
60 60
             bases=(models.Model,),
61 61
         ),
62 62
         migrations.CreateModel(
63 63
             name='Line',
64 64
             fields=[
65
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
66
-                ('partner_name', models.CharField(max_length=128, verbose_name='Partner name', blank=True)),
67
-                ('partner_sku', models.CharField(max_length=128, verbose_name='Partner SKU')),
68
-                ('partner_line_reference', models.CharField(help_text='This is the item number that the partner uses within their system', max_length=128, verbose_name='Partner reference', blank=True)),
65
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
66
+                ('partner_name', models.CharField(verbose_name='Partner name', blank=True, max_length=128)),
67
+                ('partner_sku', models.CharField(verbose_name='Partner SKU', max_length=128)),
68
+                ('partner_line_reference', models.CharField(verbose_name='Partner reference', blank=True, help_text='This is the item number that the partner uses within their system', max_length=128)),
69 69
                 ('partner_line_notes', models.TextField(verbose_name='Partner Notes', blank=True)),
70
-                ('title', models.CharField(max_length=255, verbose_name='Title')),
71
-                ('upc', models.CharField(max_length=128, null=True, verbose_name='UPC', blank=True)),
72
-                ('quantity', models.PositiveIntegerField(default=1, verbose_name='Quantity')),
70
+                ('title', models.CharField(verbose_name='Title', max_length=255)),
71
+                ('upc', models.CharField(verbose_name='UPC', blank=True, null=True, max_length=128)),
72
+                ('quantity', models.PositiveIntegerField(verbose_name='Quantity', default=1)),
73 73
                 ('line_price_incl_tax', models.DecimalField(verbose_name='Price (inc. tax)', max_digits=12, decimal_places=2)),
74 74
                 ('line_price_excl_tax', models.DecimalField(verbose_name='Price (excl. tax)', max_digits=12, decimal_places=2)),
75 75
                 ('line_price_before_discounts_incl_tax', models.DecimalField(verbose_name='Price before discounts (inc. tax)', max_digits=12, decimal_places=2)),
76 76
                 ('line_price_before_discounts_excl_tax', models.DecimalField(verbose_name='Price before discounts (excl. tax)', max_digits=12, decimal_places=2)),
77
-                ('unit_cost_price', models.DecimalField(null=True, verbose_name='Unit Cost Price', max_digits=12, decimal_places=2, blank=True)),
78
-                ('unit_price_incl_tax', models.DecimalField(null=True, verbose_name='Unit Price (inc. tax)', max_digits=12, decimal_places=2, blank=True)),
79
-                ('unit_price_excl_tax', models.DecimalField(null=True, verbose_name='Unit Price (excl. tax)', max_digits=12, decimal_places=2, blank=True)),
80
-                ('unit_retail_price', models.DecimalField(null=True, verbose_name='Unit Retail Price', max_digits=12, decimal_places=2, blank=True)),
81
-                ('status', models.CharField(max_length=255, verbose_name='Status', blank=True)),
82
-                ('est_dispatch_date', models.DateField(null=True, verbose_name='Estimated Dispatch Date', blank=True)),
83
-                ('partner', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='Partner', blank=True, to='partner.Partner', null=True)),
84
-                ('product', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='Product', blank=True, to='catalogue.Product', null=True)),
85
-                ('stockrecord', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='Stock record', blank=True, to='partner.StockRecord', null=True)),
77
+                ('unit_cost_price', models.DecimalField(verbose_name='Unit Cost Price', max_digits=12, decimal_places=2, blank=True, null=True)),
78
+                ('unit_price_incl_tax', models.DecimalField(verbose_name='Unit Price (inc. tax)', max_digits=12, decimal_places=2, blank=True, null=True)),
79
+                ('unit_price_excl_tax', models.DecimalField(verbose_name='Unit Price (excl. tax)', max_digits=12, decimal_places=2, blank=True, null=True)),
80
+                ('unit_retail_price', models.DecimalField(verbose_name='Unit Retail Price', max_digits=12, decimal_places=2, blank=True, null=True)),
81
+                ('status', models.CharField(verbose_name='Status', blank=True, max_length=255)),
82
+                ('est_dispatch_date', models.DateField(verbose_name='Estimated Dispatch Date', blank=True, null=True)),
83
+                ('partner', models.ForeignKey(verbose_name='Partner', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='partner.Partner', null=True)),
84
+                ('product', models.ForeignKey(verbose_name='Product', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='catalogue.Product', null=True)),
85
+                ('stockrecord', models.ForeignKey(verbose_name='Stock record', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='partner.StockRecord', null=True)),
86 86
             ],
87 87
             options={
88
-                'abstract': False,
89 88
                 'verbose_name': 'Order Line',
90 89
                 'verbose_name_plural': 'Order Lines',
90
+                'abstract': False,
91 91
             },
92 92
             bases=(models.Model,),
93 93
         ),
94 94
         migrations.CreateModel(
95 95
             name='LineAttribute',
96 96
             fields=[
97
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
98
-                ('type', models.CharField(max_length=128, verbose_name='Type')),
99
-                ('value', models.CharField(max_length=255, verbose_name='Value')),
97
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
98
+                ('type', models.CharField(verbose_name='Type', max_length=128)),
99
+                ('value', models.CharField(verbose_name='Value', max_length=255)),
100 100
                 ('line', models.ForeignKey(verbose_name='Line', to='order.Line')),
101
-                ('option', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='Option', to='catalogue.Option', null=True)),
101
+                ('option', models.ForeignKey(verbose_name='Option', on_delete=django.db.models.deletion.SET_NULL, to='catalogue.Option', null=True)),
102 102
             ],
103 103
             options={
104
-                'abstract': False,
105 104
                 'verbose_name': 'Line Attribute',
106 105
                 'verbose_name_plural': 'Line Attributes',
106
+                'abstract': False,
107 107
             },
108 108
             bases=(models.Model,),
109 109
         ),
110 110
         migrations.CreateModel(
111 111
             name='LinePrice',
112 112
             fields=[
113
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
114
-                ('quantity', models.PositiveIntegerField(default=1, verbose_name='Quantity')),
113
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
114
+                ('quantity', models.PositiveIntegerField(verbose_name='Quantity', default=1)),
115 115
                 ('price_incl_tax', models.DecimalField(verbose_name='Price (inc. tax)', max_digits=12, decimal_places=2)),
116 116
                 ('price_excl_tax', models.DecimalField(verbose_name='Price (excl. tax)', max_digits=12, decimal_places=2)),
117
-                ('shipping_incl_tax', models.DecimalField(default=0, verbose_name='Shiping (inc. tax)', max_digits=12, decimal_places=2)),
118
-                ('shipping_excl_tax', models.DecimalField(default=0, verbose_name='Shipping (excl. tax)', max_digits=12, decimal_places=2)),
117
+                ('shipping_incl_tax', models.DecimalField(verbose_name='Shiping (inc. tax)', max_digits=12, decimal_places=2, default=0)),
118
+                ('shipping_excl_tax', models.DecimalField(verbose_name='Shipping (excl. tax)', max_digits=12, decimal_places=2, default=0)),
119 119
                 ('line', models.ForeignKey(verbose_name='Line', to='order.Line')),
120 120
             ],
121 121
             options={
122
-                'ordering': (b'id',),
123
-                'abstract': False,
124 122
                 'verbose_name': 'Line Price',
125 123
                 'verbose_name_plural': 'Line Prices',
124
+                'ordering': ('id',),
125
+                'abstract': False,
126 126
             },
127 127
             bases=(models.Model,),
128 128
         ),
129 129
         migrations.CreateModel(
130 130
             name='Order',
131 131
             fields=[
132
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
133
-                ('number', models.CharField(unique=True, max_length=128, verbose_name='Order number', db_index=True)),
134
-                ('currency', models.CharField(default=b'GBP', max_length=12, verbose_name='Currency')),
132
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
133
+                ('number', models.CharField(verbose_name='Order number', unique=True, db_index=True, max_length=128)),
134
+                ('currency', models.CharField(verbose_name='Currency', default='GBP', max_length=12)),
135 135
                 ('total_incl_tax', models.DecimalField(verbose_name='Order total (inc. tax)', max_digits=12, decimal_places=2)),
136 136
                 ('total_excl_tax', models.DecimalField(verbose_name='Order total (excl. tax)', max_digits=12, decimal_places=2)),
137
-                ('shipping_incl_tax', models.DecimalField(default=0, verbose_name='Shipping charge (inc. tax)', max_digits=12, decimal_places=2)),
138
-                ('shipping_excl_tax', models.DecimalField(default=0, verbose_name='Shipping charge (excl. tax)', max_digits=12, decimal_places=2)),
139
-                ('shipping_method', models.CharField(max_length=128, verbose_name='Shipping method', blank=True)),
140
-                ('shipping_code', models.CharField(default=b'', max_length=128, blank=True)),
141
-                ('status', models.CharField(max_length=100, verbose_name='Status', blank=True)),
142
-                ('guest_email', models.EmailField(max_length=75, verbose_name='Guest email address', blank=True)),
137
+                ('shipping_incl_tax', models.DecimalField(verbose_name='Shipping charge (inc. tax)', max_digits=12, decimal_places=2, default=0)),
138
+                ('shipping_excl_tax', models.DecimalField(verbose_name='Shipping charge (excl. tax)', max_digits=12, decimal_places=2, default=0)),
139
+                ('shipping_method', models.CharField(verbose_name='Shipping method', blank=True, max_length=128)),
140
+                ('shipping_code', models.CharField(blank=True, default='', max_length=128)),
141
+                ('status', models.CharField(verbose_name='Status', blank=True, max_length=100)),
142
+                ('guest_email', models.EmailField(verbose_name='Guest email address', blank=True, max_length=75)),
143 143
                 ('date_placed', models.DateTimeField(auto_now_add=True, db_index=True)),
144
-                ('basket', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='Basket', blank=True, to='basket.Basket', null=True)),
145
-                ('billing_address', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='Billing Address', blank=True, to='order.BillingAddress', null=True)),
146
-                ('site', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='Site', to='sites.Site', null=True)),
147
-                ('user', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='User', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
144
+                ('basket', models.ForeignKey(verbose_name='Basket', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='basket.Basket', null=True)),
145
+                ('billing_address', models.ForeignKey(verbose_name='Billing Address', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='order.BillingAddress', null=True)),
146
+                ('site', models.ForeignKey(verbose_name='Site', on_delete=django.db.models.deletion.SET_NULL, to='sites.Site', null=True)),
147
+                ('user', models.ForeignKey(verbose_name='User', on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
148 148
             ],
149 149
             options={
150
-                'ordering': [b'-date_placed'],
151
-                'abstract': False,
152 150
                 'verbose_name': 'Order',
153 151
                 'verbose_name_plural': 'Orders',
152
+                'ordering': ['-date_placed'],
153
+                'abstract': False,
154 154
             },
155 155
             bases=(models.Model,),
156 156
         ),
@@ -175,63 +175,63 @@ class Migration(migrations.Migration):
175 175
         migrations.CreateModel(
176 176
             name='OrderDiscount',
177 177
             fields=[
178
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
179
-                ('category', models.CharField(default=b'Basket', max_length=64, verbose_name='Discount category', choices=[(b'Basket', 'Basket'), (b'Shipping', 'Shipping'), (b'Deferred', 'Deferred')])),
180
-                ('offer_id', models.PositiveIntegerField(null=True, verbose_name='Offer ID', blank=True)),
181
-                ('offer_name', models.CharField(db_index=True, max_length=128, verbose_name='Offer name', blank=True)),
182
-                ('voucher_id', models.PositiveIntegerField(null=True, verbose_name='Voucher ID', blank=True)),
183
-                ('voucher_code', models.CharField(db_index=True, max_length=128, verbose_name='Code', blank=True)),
184
-                ('frequency', models.PositiveIntegerField(null=True, verbose_name='Frequency')),
185
-                ('amount', models.DecimalField(default=0, verbose_name='Amount', max_digits=12, decimal_places=2)),
178
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
179
+                ('category', models.CharField(verbose_name='Discount category', choices=[('Basket', 'Basket'), ('Shipping', 'Shipping'), ('Deferred', 'Deferred')], default='Basket', max_length=64)),
180
+                ('offer_id', models.PositiveIntegerField(verbose_name='Offer ID', blank=True, null=True)),
181
+                ('offer_name', models.CharField(verbose_name='Offer name', blank=True, db_index=True, max_length=128)),
182
+                ('voucher_id', models.PositiveIntegerField(verbose_name='Voucher ID', blank=True, null=True)),
183
+                ('voucher_code', models.CharField(verbose_name='Code', blank=True, db_index=True, max_length=128)),
184
+                ('frequency', models.PositiveIntegerField(verbose_name='Frequency', null=True)),
185
+                ('amount', models.DecimalField(verbose_name='Amount', max_digits=12, decimal_places=2, default=0)),
186 186
                 ('message', models.TextField(blank=True)),
187 187
                 ('order', models.ForeignKey(verbose_name='Order', to='order.Order')),
188 188
             ],
189 189
             options={
190
-                'abstract': False,
191 190
                 'verbose_name': 'Order Discount',
192 191
                 'verbose_name_plural': 'Order Discounts',
192
+                'abstract': False,
193 193
             },
194 194
             bases=(models.Model,),
195 195
         ),
196 196
         migrations.CreateModel(
197 197
             name='OrderNote',
198 198
             fields=[
199
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
200
-                ('note_type', models.CharField(max_length=128, verbose_name='Note Type', blank=True)),
199
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
200
+                ('note_type', models.CharField(verbose_name='Note Type', blank=True, max_length=128)),
201 201
                 ('message', models.TextField(verbose_name='Message')),
202
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
203
-                ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date Updated')),
202
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
203
+                ('date_updated', models.DateTimeField(verbose_name='Date Updated', auto_now=True)),
204 204
                 ('order', models.ForeignKey(verbose_name='Order', to='order.Order')),
205 205
                 ('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL, null=True)),
206 206
             ],
207 207
             options={
208
-                'abstract': False,
209 208
                 'verbose_name': 'Order Note',
210 209
                 'verbose_name_plural': 'Order Notes',
210
+                'abstract': False,
211 211
             },
212 212
             bases=(models.Model,),
213 213
         ),
214 214
         migrations.CreateModel(
215 215
             name='PaymentEvent',
216 216
             fields=[
217
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
217
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
218 218
                 ('amount', models.DecimalField(verbose_name='Amount', max_digits=12, decimal_places=2)),
219
-                ('reference', models.CharField(max_length=128, verbose_name='Reference', blank=True)),
220
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
219
+                ('reference', models.CharField(verbose_name='Reference', blank=True, max_length=128)),
220
+                ('date_created', models.DateTimeField(verbose_name='Date created', auto_now_add=True)),
221 221
                 ('order', models.ForeignKey(verbose_name='Order', to='order.Order')),
222 222
             ],
223 223
             options={
224
-                'ordering': [b'-date_created'],
225
-                'abstract': False,
226 224
                 'verbose_name': 'Payment Event',
227 225
                 'verbose_name_plural': 'Payment Events',
226
+                'ordering': ['-date_created'],
227
+                'abstract': False,
228 228
             },
229 229
             bases=(models.Model,),
230 230
         ),
231 231
         migrations.CreateModel(
232 232
             name='PaymentEventQuantity',
233 233
             fields=[
234
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
234
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
235 235
                 ('quantity', models.PositiveIntegerField(verbose_name='Quantity')),
236 236
             ],
237 237
             options={
@@ -243,7 +243,7 @@ class Migration(migrations.Migration):
243 243
         migrations.AddField(
244 244
             model_name='paymentevent',
245 245
             name='lines',
246
-            field=models.ManyToManyField(to='order.Line', verbose_name='Lines', through='order.PaymentEventQuantity'),
246
+            field=models.ManyToManyField(verbose_name='Lines', through='order.PaymentEventQuantity', to='order.Line'),
247 247
             preserve_default=True,
248 248
         ),
249 249
         migrations.AddField(
@@ -260,20 +260,20 @@ class Migration(migrations.Migration):
260 260
         ),
261 261
         migrations.AlterUniqueTogether(
262 262
             name='paymenteventquantity',
263
-            unique_together=set([(b'event', b'line')]),
263
+            unique_together=set([('event', 'line')]),
264 264
         ),
265 265
         migrations.CreateModel(
266 266
             name='PaymentEventType',
267 267
             fields=[
268
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
269
-                ('name', models.CharField(unique=True, max_length=128, verbose_name='Name')),
270
-                ('code', oscar.models.fields.autoslugfield.AutoSlugField(populate_from=b'name', editable=False, max_length=128, blank=True, unique=True, verbose_name='Code')),
268
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
269
+                ('name', models.CharField(verbose_name='Name', unique=True, max_length=128)),
270
+                ('code', oscar.models.fields.autoslugfield.AutoSlugField(editable=False, verbose_name='Code', blank=True, max_length=128, populate_from='name', unique=True)),
271 271
             ],
272 272
             options={
273
-                'ordering': (b'name',),
274
-                'abstract': False,
275 273
                 'verbose_name': 'Payment Event Type',
276 274
                 'verbose_name_plural': 'Payment Event Types',
275
+                'ordering': ('name',),
276
+                'abstract': False,
277 277
             },
278 278
             bases=(models.Model,),
279 279
         ),
@@ -286,47 +286,47 @@ class Migration(migrations.Migration):
286 286
         migrations.CreateModel(
287 287
             name='ShippingAddress',
288 288
             fields=[
289
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
290
-                ('title', models.CharField(blank=True, max_length=64, verbose_name='Title', choices=[(b'Mr', 'Mr'), (b'Miss', 'Miss'), (b'Mrs', 'Mrs'), (b'Ms', 'Ms'), (b'Dr', 'Dr')])),
291
-                ('first_name', models.CharField(max_length=255, verbose_name='First name', blank=True)),
292
-                ('last_name', models.CharField(max_length=255, verbose_name='Last name', blank=True)),
293
-                ('line1', models.CharField(max_length=255, verbose_name='First line of address')),
294
-                ('line2', models.CharField(max_length=255, verbose_name='Second line of address', blank=True)),
295
-                ('line3', models.CharField(max_length=255, verbose_name='Third line of address', blank=True)),
296
-                ('line4', models.CharField(max_length=255, verbose_name='City', blank=True)),
297
-                ('state', models.CharField(max_length=255, verbose_name='State/County', blank=True)),
298
-                ('postcode', oscar.models.fields.UppercaseCharField(max_length=64, verbose_name='Post/Zip-code', blank=True)),
299
-                ('search_text', models.TextField(verbose_name='Search text - used only for searching addresses', editable=False)),
300
-                ('phone_number', oscar.models.fields.PhoneNumberField(help_text='In case we need to call you about your order', verbose_name='Phone number', blank=True)),
301
-                ('notes', models.TextField(help_text='Tell us anything we should know when delivering your order.', verbose_name='Instructions', blank=True)),
289
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
290
+                ('title', models.CharField(verbose_name='Title', choices=[('Mr', 'Mr'), ('Miss', 'Miss'), ('Mrs', 'Mrs'), ('Ms', 'Ms'), ('Dr', 'Dr')], blank=True, max_length=64)),
291
+                ('first_name', models.CharField(verbose_name='First name', blank=True, max_length=255)),
292
+                ('last_name', models.CharField(verbose_name='Last name', blank=True, max_length=255)),
293
+                ('line1', models.CharField(verbose_name='First line of address', max_length=255)),
294
+                ('line2', models.CharField(verbose_name='Second line of address', blank=True, max_length=255)),
295
+                ('line3', models.CharField(verbose_name='Third line of address', blank=True, max_length=255)),
296
+                ('line4', models.CharField(verbose_name='City', blank=True, max_length=255)),
297
+                ('state', models.CharField(verbose_name='State/County', blank=True, max_length=255)),
298
+                ('postcode', oscar.models.fields.UppercaseCharField(verbose_name='Post/Zip-code', blank=True, max_length=64)),
299
+                ('search_text', models.TextField(editable=False, verbose_name='Search text - used only for searching addresses')),
300
+                ('phone_number', oscar.models.fields.PhoneNumberField(verbose_name='Phone number', blank=True, help_text='In case we need to call you about your order')),
301
+                ('notes', models.TextField(verbose_name='Instructions', blank=True, help_text='Tell us anything we should know when delivering your order.')),
302 302
                 ('country', models.ForeignKey(verbose_name='Country', to='address.Country')),
303 303
             ],
304 304
             options={
305
-                'abstract': False,
306 305
                 'verbose_name': 'Shipping address',
307 306
                 'verbose_name_plural': 'Shipping addresses',
307
+                'abstract': False,
308 308
             },
309 309
             bases=(models.Model,),
310 310
         ),
311 311
         migrations.AddField(
312 312
             model_name='order',
313 313
             name='shipping_address',
314
-            field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='Shipping Address', blank=True, to='order.ShippingAddress', null=True),
314
+            field=models.ForeignKey(verbose_name='Shipping Address', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='order.ShippingAddress', null=True),
315 315
             preserve_default=True,
316 316
         ),
317 317
         migrations.CreateModel(
318 318
             name='ShippingEvent',
319 319
             fields=[
320
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
321
-                ('notes', models.TextField(help_text='This could be the dispatch reference, or a tracking number', verbose_name='Event notes', blank=True)),
322
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
320
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
321
+                ('notes', models.TextField(verbose_name='Event notes', blank=True, help_text='This could be the dispatch reference, or a tracking number')),
322
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
323 323
                 ('order', models.ForeignKey(verbose_name='Order', to='order.Order')),
324 324
             ],
325 325
             options={
326
-                'ordering': [b'-date_created'],
327
-                'abstract': False,
328 326
                 'verbose_name': 'Shipping Event',
329 327
                 'verbose_name_plural': 'Shipping Events',
328
+                'ordering': ['-date_created'],
329
+                'abstract': False,
330 330
             },
331 331
             bases=(models.Model,),
332 332
         ),
@@ -339,7 +339,7 @@ class Migration(migrations.Migration):
339 339
         migrations.CreateModel(
340 340
             name='ShippingEventQuantity',
341 341
             fields=[
342
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
342
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
343 343
                 ('quantity', models.PositiveIntegerField(verbose_name='Quantity')),
344 344
             ],
345 345
             options={
@@ -351,7 +351,7 @@ class Migration(migrations.Migration):
351 351
         migrations.AddField(
352 352
             model_name='shippingevent',
353 353
             name='lines',
354
-            field=models.ManyToManyField(to='order.Line', verbose_name='Lines', through='order.ShippingEventQuantity'),
354
+            field=models.ManyToManyField(verbose_name='Lines', through='order.ShippingEventQuantity', to='order.Line'),
355 355
             preserve_default=True,
356 356
         ),
357 357
         migrations.AddField(
@@ -368,20 +368,20 @@ class Migration(migrations.Migration):
368 368
         ),
369 369
         migrations.AlterUniqueTogether(
370 370
             name='shippingeventquantity',
371
-            unique_together=set([(b'event', b'line')]),
371
+            unique_together=set([('event', 'line')]),
372 372
         ),
373 373
         migrations.CreateModel(
374 374
             name='ShippingEventType',
375 375
             fields=[
376
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
377
-                ('name', models.CharField(unique=True, max_length=255, verbose_name='Name')),
378
-                ('code', oscar.models.fields.autoslugfield.AutoSlugField(populate_from=b'name', editable=False, max_length=128, blank=True, unique=True, verbose_name='Code')),
376
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
377
+                ('name', models.CharField(verbose_name='Name', unique=True, max_length=255)),
378
+                ('code', oscar.models.fields.autoslugfield.AutoSlugField(editable=False, verbose_name='Code', blank=True, max_length=128, populate_from='name', unique=True)),
379 379
             ],
380 380
             options={
381
-                'ordering': (b'name',),
382
-                'abstract': False,
383 381
                 'verbose_name': 'Shipping Event Type',
384 382
                 'verbose_name_plural': 'Shipping Event Types',
383
+                'ordering': ('name',),
384
+                'abstract': False,
385 385
             },
386 386
             bases=(models.Model,),
387 387
         ),

+ 2
- 2
oscar/apps/order/processing.py Visa fil

@@ -38,7 +38,7 @@ class EventHandler(object):
38 38
         # Example implementation
39 39
         self.validate_shipping_event(
40 40
             order, event_type, lines, line_quantities, **kwargs)
41
-        self.create_shipping_event(
41
+        return self.create_shipping_event(
42 42
             order, event_type, lines, line_quantities, **kwargs)
43 43
 
44 44
     def handle_payment_event(self, order, event_type, amount, lines=None,
@@ -53,7 +53,7 @@ class EventHandler(object):
53 53
         """
54 54
         self.validate_payment_event(
55 55
             order, event_type, amount, lines, line_quantities, **kwargs)
56
-        self.create_payment_event(
56
+        return self.create_payment_event(
57 57
             order, event_type, amount, lines, line_quantities, **kwargs)
58 58
 
59 59
     def handle_order_status_change(self, order, new_status, note_msg=None):

+ 7
- 3
oscar/apps/partner/abstract_models.py Visa fil

@@ -1,5 +1,6 @@
1 1
 from django.db import models
2 2
 from django.conf import settings
3
+from django.utils.encoding import python_2_unicode_compatible
3 4
 from django.utils.translation import ugettext_lazy as _, pgettext_lazy
4 5
 
5 6
 from oscar.core.compat import AUTH_USER_MODEL
@@ -7,6 +8,7 @@ from oscar.models.fields import AutoSlugField
7 8
 from oscar.apps.partner.exceptions import InvalidStockAdjustment
8 9
 
9 10
 
11
+@python_2_unicode_compatible
10 12
 class AbstractPartner(models.Model):
11 13
     """
12 14
     A fulfillment partner. An individual or company who can fulfil products.
@@ -68,10 +70,11 @@ class AbstractPartner(models.Model):
68 70
         verbose_name = _('Fulfillment partner')
69 71
         verbose_name_plural = _('Fulfillment partners')
70 72
 
71
-    def __unicode__(self):
73
+    def __str__(self):
72 74
         return self.display_name
73 75
 
74 76
 
77
+@python_2_unicode_compatible
75 78
 class AbstractStockRecord(models.Model):
76 79
     """
77 80
     A stock record.
@@ -142,7 +145,7 @@ class AbstractStockRecord(models.Model):
142 145
     date_updated = models.DateTimeField(_("Date updated"), auto_now=True,
143 146
                                         db_index=True)
144 147
 
145
-    def __unicode__(self):
148
+    def __str__(self):
146 149
         msg = u"Partner: %s, product: %s" % (
147 150
             self.partner.display_name, self.product,)
148 151
         if self.partner_sku:
@@ -221,6 +224,7 @@ class AbstractStockRecord(models.Model):
221 224
         return self.net_stock_level < self.low_stock_threshold
222 225
 
223 226
 
227
+@python_2_unicode_compatible
224 228
 class AbstractStockAlert(models.Model):
225 229
     """
226 230
     A stock alert. E.g. used to notify users when a product is 'back in stock'.
@@ -244,7 +248,7 @@ class AbstractStockAlert(models.Model):
244 248
         self.save()
245 249
     close.alters_data = True
246 250
 
247
-    def __unicode__(self):
251
+    def __str__(self):
248 252
         return _('<stockalert for "%(stock)s" status %(status)s>') \
249 253
             % {'stock': self.stockrecord, 'status': self.status}
250 254
 

+ 38
- 38
oscar/apps/partner/migrations/0001_initial.py Visa fil

@@ -2,9 +2,9 @@
2 2
 from __future__ import unicode_literals
3 3
 
4 4
 from django.db import models, migrations
5
+import oscar.models.fields
5 6
 import oscar.models.fields.autoslugfield
6 7
 from django.conf import settings
7
-import oscar.models.fields
8 8
 
9 9
 
10 10
 class Migration(migrations.Migration):
@@ -19,81 +19,81 @@ class Migration(migrations.Migration):
19 19
         migrations.CreateModel(
20 20
             name='Partner',
21 21
             fields=[
22
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
23
-                ('code', oscar.models.fields.autoslugfield.AutoSlugField(populate_from=b'name', editable=False, max_length=128, blank=True, unique=True, verbose_name='Code')),
24
-                ('name', models.CharField(max_length=128, verbose_name='Name', blank=True)),
25
-                ('users', models.ManyToManyField(to=settings.AUTH_USER_MODEL, null=True, verbose_name='Users', blank=True)),
22
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
23
+                ('code', oscar.models.fields.autoslugfield.AutoSlugField(editable=False, verbose_name='Code', blank=True, max_length=128, populate_from='name', unique=True)),
24
+                ('name', models.CharField(verbose_name='Name', blank=True, max_length=128)),
25
+                ('users', models.ManyToManyField(verbose_name='Users', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
26 26
             ],
27 27
             options={
28
-                'abstract': False,
29 28
                 'verbose_name': 'Fulfillment partner',
30 29
                 'verbose_name_plural': 'Fulfillment partners',
31
-                'permissions': ((b'dashboard_access', b'Can access dashboard'),),
30
+                'permissions': (('dashboard_access', 'Can access dashboard'),),
31
+                'abstract': False,
32 32
             },
33 33
             bases=(models.Model,),
34 34
         ),
35 35
         migrations.CreateModel(
36 36
             name='PartnerAddress',
37 37
             fields=[
38
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
39
-                ('title', models.CharField(blank=True, max_length=64, verbose_name='Title', choices=[(b'Mr', 'Mr'), (b'Miss', 'Miss'), (b'Mrs', 'Mrs'), (b'Ms', 'Ms'), (b'Dr', 'Dr')])),
40
-                ('first_name', models.CharField(max_length=255, verbose_name='First name', blank=True)),
41
-                ('last_name', models.CharField(max_length=255, verbose_name='Last name', blank=True)),
42
-                ('line1', models.CharField(max_length=255, verbose_name='First line of address')),
43
-                ('line2', models.CharField(max_length=255, verbose_name='Second line of address', blank=True)),
44
-                ('line3', models.CharField(max_length=255, verbose_name='Third line of address', blank=True)),
45
-                ('line4', models.CharField(max_length=255, verbose_name='City', blank=True)),
46
-                ('state', models.CharField(max_length=255, verbose_name='State/County', blank=True)),
47
-                ('postcode', oscar.models.fields.UppercaseCharField(max_length=64, verbose_name='Post/Zip-code', blank=True)),
48
-                ('search_text', models.TextField(verbose_name='Search text - used only for searching addresses', editable=False)),
38
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
39
+                ('title', models.CharField(verbose_name='Title', choices=[('Mr', 'Mr'), ('Miss', 'Miss'), ('Mrs', 'Mrs'), ('Ms', 'Ms'), ('Dr', 'Dr')], blank=True, max_length=64)),
40
+                ('first_name', models.CharField(verbose_name='First name', blank=True, max_length=255)),
41
+                ('last_name', models.CharField(verbose_name='Last name', blank=True, max_length=255)),
42
+                ('line1', models.CharField(verbose_name='First line of address', max_length=255)),
43
+                ('line2', models.CharField(verbose_name='Second line of address', blank=True, max_length=255)),
44
+                ('line3', models.CharField(verbose_name='Third line of address', blank=True, max_length=255)),
45
+                ('line4', models.CharField(verbose_name='City', blank=True, max_length=255)),
46
+                ('state', models.CharField(verbose_name='State/County', blank=True, max_length=255)),
47
+                ('postcode', oscar.models.fields.UppercaseCharField(verbose_name='Post/Zip-code', blank=True, max_length=64)),
48
+                ('search_text', models.TextField(editable=False, verbose_name='Search text - used only for searching addresses')),
49 49
                 ('country', models.ForeignKey(verbose_name='Country', to='address.Country')),
50 50
                 ('partner', models.ForeignKey(verbose_name='Partner', to='partner.Partner')),
51 51
             ],
52 52
             options={
53
-                'abstract': False,
54 53
                 'verbose_name': 'Partner address',
55 54
                 'verbose_name_plural': 'Partner addresses',
55
+                'abstract': False,
56 56
             },
57 57
             bases=(models.Model,),
58 58
         ),
59 59
         migrations.CreateModel(
60 60
             name='StockAlert',
61 61
             fields=[
62
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
62
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
63 63
                 ('threshold', models.PositiveIntegerField(verbose_name='Threshold')),
64
-                ('status', models.CharField(default=b'Open', max_length=128, verbose_name='Status', choices=[(b'Open', 'Open'), (b'Closed', 'Closed')])),
65
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
66
-                ('date_closed', models.DateTimeField(null=True, verbose_name='Date Closed', blank=True)),
64
+                ('status', models.CharField(verbose_name='Status', choices=[('Open', 'Open'), ('Closed', 'Closed')], default='Open', max_length=128)),
65
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
66
+                ('date_closed', models.DateTimeField(verbose_name='Date Closed', blank=True, null=True)),
67 67
             ],
68 68
             options={
69
-                'ordering': (b'-date_created',),
70
-                'abstract': False,
71 69
                 'verbose_name': 'Stock alert',
72 70
                 'verbose_name_plural': 'Stock alerts',
71
+                'ordering': ('-date_created',),
72
+                'abstract': False,
73 73
             },
74 74
             bases=(models.Model,),
75 75
         ),
76 76
         migrations.CreateModel(
77 77
             name='StockRecord',
78 78
             fields=[
79
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
80
-                ('partner_sku', models.CharField(max_length=128, verbose_name='Partner SKU')),
81
-                ('price_currency', models.CharField(default=b'GBP', max_length=12, verbose_name='Currency')),
82
-                ('price_excl_tax', models.DecimalField(null=True, verbose_name='Price (excl. tax)', max_digits=12, decimal_places=2, blank=True)),
83
-                ('price_retail', models.DecimalField(null=True, verbose_name='Price (retail)', max_digits=12, decimal_places=2, blank=True)),
84
-                ('cost_price', models.DecimalField(null=True, verbose_name='Cost Price', max_digits=12, decimal_places=2, blank=True)),
85
-                ('num_in_stock', models.PositiveIntegerField(null=True, verbose_name='Number in stock', blank=True)),
86
-                ('num_allocated', models.IntegerField(null=True, verbose_name='Number allocated', blank=True)),
87
-                ('low_stock_threshold', models.PositiveIntegerField(null=True, verbose_name='Low Stock Threshold', blank=True)),
88
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
89
-                ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated', db_index=True)),
79
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
80
+                ('partner_sku', models.CharField(verbose_name='Partner SKU', max_length=128)),
81
+                ('price_currency', models.CharField(verbose_name='Currency', default='GBP', max_length=12)),
82
+                ('price_excl_tax', models.DecimalField(verbose_name='Price (excl. tax)', max_digits=12, decimal_places=2, blank=True, null=True)),
83
+                ('price_retail', models.DecimalField(verbose_name='Price (retail)', max_digits=12, decimal_places=2, blank=True, null=True)),
84
+                ('cost_price', models.DecimalField(verbose_name='Cost Price', max_digits=12, decimal_places=2, blank=True, null=True)),
85
+                ('num_in_stock', models.PositiveIntegerField(verbose_name='Number in stock', blank=True, null=True)),
86
+                ('num_allocated', models.IntegerField(verbose_name='Number allocated', blank=True, null=True)),
87
+                ('low_stock_threshold', models.PositiveIntegerField(verbose_name='Low Stock Threshold', blank=True, null=True)),
88
+                ('date_created', models.DateTimeField(verbose_name='Date created', auto_now_add=True)),
89
+                ('date_updated', models.DateTimeField(verbose_name='Date updated', auto_now=True, db_index=True)),
90 90
                 ('partner', models.ForeignKey(verbose_name='Partner', to='partner.Partner')),
91 91
                 ('product', models.ForeignKey(verbose_name='Product', to='catalogue.Product')),
92 92
             ],
93 93
             options={
94
-                'abstract': False,
95 94
                 'verbose_name': 'Stock record',
96 95
                 'verbose_name_plural': 'Stock records',
96
+                'abstract': False,
97 97
             },
98 98
             bases=(models.Model,),
99 99
         ),
@@ -105,6 +105,6 @@ class Migration(migrations.Migration):
105 105
         ),
106 106
         migrations.AlterUniqueTogether(
107 107
             name='stockrecord',
108
-            unique_together=set([(b'partner', b'partner_sku')]),
108
+            unique_together=set([('partner', 'partner_sku')]),
109 109
         ),
110 110
     ]

+ 9
- 4
oscar/apps/payment/abstract_models.py Visa fil

@@ -1,6 +1,7 @@
1 1
 from decimal import Decimal
2 2
 
3 3
 from django.db import models
4
+from django.utils.encoding import python_2_unicode_compatible
4 5
 from django.utils.translation import ugettext_lazy as _
5 6
 from django.conf import settings
6 7
 
@@ -11,6 +12,7 @@ from oscar.models.fields import AutoSlugField
11 12
 from . import bankcards
12 13
 
13 14
 
15
+@python_2_unicode_compatible
14 16
 class AbstractTransaction(models.Model):
15 17
     """
16 18
     A transaction for a particular payment source.
@@ -39,7 +41,7 @@ class AbstractTransaction(models.Model):
39 41
     status = models.CharField(_("Status"), max_length=128, blank=True)
40 42
     date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)
41 43
 
42
-    def __unicode__(self):
44
+    def __str__(self):
43 45
         return _(u"%(type)s of %(amount).2f") % {
44 46
             'type': self.txn_type,
45 47
             'amount': self.amount}
@@ -52,6 +54,7 @@ class AbstractTransaction(models.Model):
52 54
         verbose_name_plural = _("Transactions")
53 55
 
54 56
 
57
+@python_2_unicode_compatible
55 58
 class AbstractSource(models.Model):
56 59
     """
57 60
     A source of payment for an order.
@@ -104,7 +107,7 @@ class AbstractSource(models.Model):
104 107
         verbose_name = _("Source")
105 108
         verbose_name_plural = _("Sources")
106 109
 
107
-    def __unicode__(self):
110
+    def __str__(self):
108 111
         description = _("Allocation of %(amount)s from type %(type)s") % {
109 112
             'amount': currency(self.amount_allocated, self.currency),
110 113
             'type': self.source_type}
@@ -191,6 +194,7 @@ class AbstractSource(models.Model):
191 194
         return self.amount_debited - self.amount_refunded
192 195
 
193 196
 
197
+@python_2_unicode_compatible
194 198
 class AbstractSourceType(models.Model):
195 199
     """
196 200
     A type of payment source.
@@ -209,10 +213,11 @@ class AbstractSourceType(models.Model):
209 213
         verbose_name = _("Source Type")
210 214
         verbose_name_plural = _("Source Types")
211 215
 
212
-    def __unicode__(self):
216
+    def __str__(self):
213 217
         return self.name
214 218
 
215 219
 
220
+@python_2_unicode_compatible
216 221
 class AbstractBankcard(models.Model):
217 222
     """
218 223
     Model representing a user's bankcard.  This is used for two purposes:
@@ -256,7 +261,7 @@ class AbstractBankcard(models.Model):
256 261
     issue_number = None
257 262
     ccv = None
258 263
 
259
-    def __unicode__(self):
264
+    def __str__(self):
260 265
         return _(u"%(card_type)s %(number)s (Expires: %(expiry)s)") % {
261 266
             'card_type': self.card_type,
262 267
             'number': self.number,

+ 1
- 1
oscar/apps/payment/bankcards.py Visa fil

@@ -1,4 +1,4 @@
1
-from six.moves import map
1
+from django.utils.six.moves import map
2 2
 VISA, VISA_ELECTRON, MASTERCARD, AMEX, MAESTRO, DISCOVER = (
3 3
     'Visa', 'Visa Electron', 'Mastercard', 'American Express',
4 4
     'Maestro', 'Discover')

+ 4
- 5
oscar/apps/payment/forms.py Visa fil

@@ -1,7 +1,6 @@
1 1
 from datetime import date
2 2
 from calendar import monthrange
3 3
 import re
4
-import six
5 4
 
6 5
 from django import forms
7 6
 from django.core.exceptions import ImproperlyConfigured
@@ -124,10 +123,10 @@ class BankcardExpiryMonthField(BankcardMonthField):
124 123
         super(BankcardExpiryMonthField, self).__init__(*args, **_kwargs)
125 124
 
126 125
     def month_choices(self):
127
-        return [("%.2d" % x, "%.2d" % x) for x in six.moves.xrange(1, 13)]
126
+        return [("%.2d" % x, "%.2d" % x) for x in range(1, 13)]
128 127
 
129 128
     def year_choices(self):
130
-        return [(x, x) for x in six.moves.xrange(
129
+        return [(x, x) for x in range(
131 130
             date.today().year,
132 131
             date.today().year + self.num_years)]
133 132
 
@@ -165,13 +164,13 @@ class BankcardStartingMonthField(BankcardMonthField):
165 164
         super(BankcardStartingMonthField, self).__init__(*args, **_kwargs)
166 165
 
167 166
     def month_choices(self):
168
-        months = [("%.2d" % x, "%.2d" % x) for x in six.moves.xrange(1, 13)]
167
+        months = [("%.2d" % x, "%.2d" % x) for x in range(1, 13)]
169 168
         months.insert(0, ("", "--"))
170 169
         return months
171 170
 
172 171
     def year_choices(self):
173 172
         today = date.today()
174
-        years = [(x, x) for x in six.moves.xrange(
173
+        years = [(x, x) for x in range(
175 174
             today.year - self.num_years,
176 175
             today.year + 1)]
177 176
         years.insert(0, ("", "--"))

+ 26
- 26
oscar/apps/payment/migrations/0001_initial.py Visa fil

@@ -10,59 +10,59 @@ from decimal import Decimal
10 10
 class Migration(migrations.Migration):
11 11
 
12 12
     dependencies = [
13
-        ('order', '0001_initial'),
14 13
         migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+        ('order', '0001_initial'),
15 15
     ]
16 16
 
17 17
     operations = [
18 18
         migrations.CreateModel(
19 19
             name='Bankcard',
20 20
             fields=[
21
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
22
-                ('card_type', models.CharField(max_length=128, verbose_name='Card Type')),
23
-                ('name', models.CharField(max_length=255, verbose_name='Name', blank=True)),
24
-                ('number', models.CharField(max_length=32, verbose_name='Number')),
21
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
22
+                ('card_type', models.CharField(verbose_name='Card Type', max_length=128)),
23
+                ('name', models.CharField(verbose_name='Name', blank=True, max_length=255)),
24
+                ('number', models.CharField(verbose_name='Number', max_length=32)),
25 25
                 ('expiry_date', models.DateField(verbose_name='Expiry Date')),
26
-                ('partner_reference', models.CharField(max_length=255, verbose_name='Partner Reference', blank=True)),
26
+                ('partner_reference', models.CharField(verbose_name='Partner Reference', blank=True, max_length=255)),
27 27
                 ('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL)),
28 28
             ],
29 29
             options={
30
-                'abstract': False,
31 30
                 'verbose_name': 'Bankcard',
32 31
                 'verbose_name_plural': 'Bankcards',
32
+                'abstract': False,
33 33
             },
34 34
             bases=(models.Model,),
35 35
         ),
36 36
         migrations.CreateModel(
37 37
             name='Source',
38 38
             fields=[
39
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
40
-                ('currency', models.CharField(default=b'GBP', max_length=12, verbose_name='Currency')),
41
-                ('amount_allocated', models.DecimalField(default=Decimal('0.00'), verbose_name='Amount Allocated', max_digits=12, decimal_places=2)),
42
-                ('amount_debited', models.DecimalField(default=Decimal('0.00'), verbose_name='Amount Debited', max_digits=12, decimal_places=2)),
43
-                ('amount_refunded', models.DecimalField(default=Decimal('0.00'), verbose_name='Amount Refunded', max_digits=12, decimal_places=2)),
44
-                ('reference', models.CharField(max_length=128, verbose_name='Reference', blank=True)),
45
-                ('label', models.CharField(max_length=128, verbose_name='Label', blank=True)),
39
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
40
+                ('currency', models.CharField(verbose_name='Currency', default='GBP', max_length=12)),
41
+                ('amount_allocated', models.DecimalField(verbose_name='Amount Allocated', max_digits=12, decimal_places=2, default=Decimal('0.00'))),
42
+                ('amount_debited', models.DecimalField(verbose_name='Amount Debited', max_digits=12, decimal_places=2, default=Decimal('0.00'))),
43
+                ('amount_refunded', models.DecimalField(verbose_name='Amount Refunded', max_digits=12, decimal_places=2, default=Decimal('0.00'))),
44
+                ('reference', models.CharField(verbose_name='Reference', blank=True, max_length=128)),
45
+                ('label', models.CharField(verbose_name='Label', blank=True, max_length=128)),
46 46
                 ('order', models.ForeignKey(verbose_name='Order', to='order.Order')),
47 47
             ],
48 48
             options={
49
-                'abstract': False,
50 49
                 'verbose_name': 'Source',
51 50
                 'verbose_name_plural': 'Sources',
51
+                'abstract': False,
52 52
             },
53 53
             bases=(models.Model,),
54 54
         ),
55 55
         migrations.CreateModel(
56 56
             name='SourceType',
57 57
             fields=[
58
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
59
-                ('name', models.CharField(max_length=128, verbose_name='Name')),
60
-                ('code', oscar.models.fields.autoslugfield.AutoSlugField(populate_from=b'name', editable=False, max_length=128, blank=True, help_text='This is used within forms to identify this source type', unique=True, verbose_name='Code')),
58
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
59
+                ('name', models.CharField(verbose_name='Name', max_length=128)),
60
+                ('code', oscar.models.fields.autoslugfield.AutoSlugField(editable=False, verbose_name='Code', blank=True, max_length=128, populate_from='name', help_text='This is used within forms to identify this source type', unique=True)),
61 61
             ],
62 62
             options={
63
-                'abstract': False,
64 63
                 'verbose_name': 'Source Type',
65 64
                 'verbose_name_plural': 'Source Types',
65
+                'abstract': False,
66 66
             },
67 67
             bases=(models.Model,),
68 68
         ),
@@ -75,19 +75,19 @@ class Migration(migrations.Migration):
75 75
         migrations.CreateModel(
76 76
             name='Transaction',
77 77
             fields=[
78
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
79
-                ('txn_type', models.CharField(max_length=128, verbose_name='Type', blank=True)),
78
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
79
+                ('txn_type', models.CharField(verbose_name='Type', blank=True, max_length=128)),
80 80
                 ('amount', models.DecimalField(verbose_name='Amount', max_digits=12, decimal_places=2)),
81
-                ('reference', models.CharField(max_length=128, verbose_name='Reference', blank=True)),
82
-                ('status', models.CharField(max_length=128, verbose_name='Status', blank=True)),
83
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
81
+                ('reference', models.CharField(verbose_name='Reference', blank=True, max_length=128)),
82
+                ('status', models.CharField(verbose_name='Status', blank=True, max_length=128)),
83
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
84 84
                 ('source', models.ForeignKey(verbose_name='Source', to='payment.Source')),
85 85
             ],
86 86
             options={
87
-                'ordering': [b'-date_created'],
88
-                'abstract': False,
89 87
                 'verbose_name': 'Transaction',
90 88
                 'verbose_name_plural': 'Transactions',
89
+                'ordering': ['-date_created'],
90
+                'abstract': False,
91 91
             },
92 92
             bases=(models.Model,),
93 93
         ),

+ 0
- 8
oscar/apps/promotions/admin.py Visa fil

@@ -17,14 +17,6 @@ class PagePromotionAdmin(admin.ModelAdmin):
17 17
     list_display = ['page_url', 'content_object', 'position']
18 18
     exclude = ['clicks']
19 19
 
20
-    def get_form(self, request, obj=None, **kwargs):
21
-        form = super(PagePromotionAdmin, self).get_form(request, obj, **kwargs)
22
-        # Only allow links to models within the promotions app
23
-        form.base_fields['content_type'].queryset \
24
-            = form.base_fields['content_type'].queryset\
25
-            .filter(app_label='promotions')
26
-        return form
27
-
28 20
 
29 21
 class KeywordPromotionAdmin(admin.ModelAdmin):
30 22
     list_display = ['keyword', 'position', 'clicks']

+ 49
- 49
oscar/apps/promotions/migrations/0001_initial.py Visa fil

@@ -9,21 +9,21 @@ class Migration(migrations.Migration):
9 9
 
10 10
     dependencies = [
11 11
         ('catalogue', '0001_initial'),
12
-        ('contenttypes', '__latest__'),
12
+        ('contenttypes', '0001_initial'),
13 13
     ]
14 14
 
15 15
     operations = [
16 16
         migrations.CreateModel(
17 17
             name='AutomaticProductList',
18 18
             fields=[
19
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
20
-                ('name', models.CharField(max_length=255, verbose_name='Title')),
19
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
20
+                ('name', models.CharField(verbose_name='Title', max_length=255)),
21 21
                 ('description', models.TextField(verbose_name='Description', blank=True)),
22 22
                 ('link_url', oscar.models.fields.ExtendedURLField(verbose_name='Link URL', blank=True)),
23
-                ('link_text', models.CharField(max_length=255, verbose_name='Link text', blank=True)),
23
+                ('link_text', models.CharField(verbose_name='Link text', blank=True, max_length=255)),
24 24
                 ('date_created', models.DateTimeField(auto_now_add=True)),
25
-                ('method', models.CharField(max_length=128, verbose_name='Method', choices=[(b'Bestselling', 'Bestselling products'), (b'RecentlyAdded', 'Recently added products')])),
26
-                ('num_products', models.PositiveSmallIntegerField(default=4, verbose_name='Number of Products')),
25
+                ('method', models.CharField(verbose_name='Method', choices=[('Bestselling', 'Bestselling products'), ('RecentlyAdded', 'Recently added products')], max_length=128)),
26
+                ('num_products', models.PositiveSmallIntegerField(verbose_name='Number of Products', default=4)),
27 27
             ],
28 28
             options={
29 29
                 'verbose_name': 'Automatic product list',
@@ -34,11 +34,11 @@ class Migration(migrations.Migration):
34 34
         migrations.CreateModel(
35 35
             name='HandPickedProductList',
36 36
             fields=[
37
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
38
-                ('name', models.CharField(max_length=255, verbose_name='Title')),
37
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
38
+                ('name', models.CharField(verbose_name='Title', max_length=255)),
39 39
                 ('description', models.TextField(verbose_name='Description', blank=True)),
40 40
                 ('link_url', oscar.models.fields.ExtendedURLField(verbose_name='Link URL', blank=True)),
41
-                ('link_text', models.CharField(max_length=255, verbose_name='Link text', blank=True)),
41
+                ('link_text', models.CharField(verbose_name='Link text', blank=True, max_length=255)),
42 42
                 ('date_created', models.DateTimeField(auto_now_add=True)),
43 43
             ],
44 44
             options={
@@ -50,10 +50,10 @@ class Migration(migrations.Migration):
50 50
         migrations.CreateModel(
51 51
             name='Image',
52 52
             fields=[
53
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
54
-                ('name', models.CharField(max_length=128, verbose_name='Name')),
55
-                ('link_url', oscar.models.fields.ExtendedURLField(help_text='This is where this promotion links to', verbose_name='Link URL', blank=True)),
56
-                ('image', models.ImageField(upload_to=b'images/promotions/', max_length=255, verbose_name='Image')),
53
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
54
+                ('name', models.CharField(verbose_name='Name', max_length=128)),
55
+                ('link_url', oscar.models.fields.ExtendedURLField(verbose_name='Link URL', blank=True, help_text='This is where this promotion links to')),
56
+                ('image', models.ImageField(verbose_name='Image', upload_to='images/promotions/', max_length=255)),
57 57
                 ('date_created', models.DateTimeField(auto_now_add=True)),
58 58
             ],
59 59
             options={
@@ -65,31 +65,31 @@ class Migration(migrations.Migration):
65 65
         migrations.CreateModel(
66 66
             name='KeywordPromotion',
67 67
             fields=[
68
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
68
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
69 69
                 ('object_id', models.PositiveIntegerField()),
70
-                ('position', models.CharField(help_text=b'Position on page', max_length=100, verbose_name='Position')),
71
-                ('display_order', models.PositiveIntegerField(default=0, verbose_name='Display Order')),
72
-                ('clicks', models.PositiveIntegerField(default=0, verbose_name='Clicks')),
73
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
74
-                ('keyword', models.CharField(max_length=200, verbose_name='Keyword')),
75
-                ('filter', models.CharField(max_length=200, verbose_name='Filter', blank=True)),
70
+                ('position', models.CharField(verbose_name='Position', help_text='Position on page', max_length=100)),
71
+                ('display_order', models.PositiveIntegerField(verbose_name='Display Order', default=0)),
72
+                ('clicks', models.PositiveIntegerField(verbose_name='Clicks', default=0)),
73
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
74
+                ('keyword', models.CharField(verbose_name='Keyword', max_length=200)),
75
+                ('filter', models.CharField(verbose_name='Filter', blank=True, max_length=200)),
76 76
                 ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
77 77
             ],
78 78
             options={
79
-                'ordering': [b'-clicks'],
80
-                'abstract': False,
81 79
                 'verbose_name': 'Keyword Promotion',
82 80
                 'verbose_name_plural': 'Keyword Promotions',
81
+                'ordering': ['-clicks'],
82
+                'abstract': False,
83 83
             },
84 84
             bases=(models.Model,),
85 85
         ),
86 86
         migrations.CreateModel(
87 87
             name='MultiImage',
88 88
             fields=[
89
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
90
-                ('name', models.CharField(max_length=128, verbose_name='Name')),
89
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
90
+                ('name', models.CharField(verbose_name='Name', max_length=128)),
91 91
                 ('date_created', models.DateTimeField(auto_now_add=True)),
92
-                ('images', models.ManyToManyField(to='promotions.Image', null=True, blank=True)),
92
+                ('images', models.ManyToManyField(blank=True, to='promotions.Image', null=True)),
93 93
             ],
94 94
             options={
95 95
                 'verbose_name': 'Multi Image',
@@ -100,20 +100,20 @@ class Migration(migrations.Migration):
100 100
         migrations.CreateModel(
101 101
             name='OrderedProduct',
102 102
             fields=[
103
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
104
-                ('display_order', models.PositiveIntegerField(default=0, verbose_name='Display Order')),
103
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
104
+                ('display_order', models.PositiveIntegerField(verbose_name='Display Order', default=0)),
105 105
             ],
106 106
             options={
107
-                'ordering': (b'display_order',),
108 107
                 'verbose_name': 'Ordered product',
109 108
                 'verbose_name_plural': 'Ordered product',
109
+                'ordering': ('display_order',),
110 110
             },
111 111
             bases=(models.Model,),
112 112
         ),
113 113
         migrations.AddField(
114 114
             model_name='handpickedproductlist',
115 115
             name='products',
116
-            field=models.ManyToManyField(to='catalogue.Product', null=True, verbose_name='Products', through='promotions.OrderedProduct', blank=True),
116
+            field=models.ManyToManyField(verbose_name='Products', through='promotions.OrderedProduct', blank=True, to='catalogue.Product', null=True),
117 117
             preserve_default=True,
118 118
         ),
119 119
         migrations.AddField(
@@ -130,47 +130,47 @@ class Migration(migrations.Migration):
130 130
         ),
131 131
         migrations.AlterUniqueTogether(
132 132
             name='orderedproduct',
133
-            unique_together=set([(b'list', b'product')]),
133
+            unique_together=set([('list', 'product')]),
134 134
         ),
135 135
         migrations.CreateModel(
136 136
             name='OrderedProductList',
137 137
             fields=[
138
-                ('handpickedproductlist_ptr', models.OneToOneField(auto_created=True, primary_key=True, serialize=False, to='promotions.HandPickedProductList')),
139
-                ('display_order', models.PositiveIntegerField(default=0, verbose_name='Display Order')),
138
+                ('handpickedproductlist_ptr', models.OneToOneField(primary_key=True, serialize=False, auto_created=True, to='promotions.HandPickedProductList')),
139
+                ('display_order', models.PositiveIntegerField(verbose_name='Display Order', default=0)),
140 140
             ],
141 141
             options={
142
-                'ordering': (b'display_order',),
143 142
                 'verbose_name': 'Ordered Product List',
144 143
                 'verbose_name_plural': 'Ordered Product Lists',
144
+                'ordering': ('display_order',),
145 145
             },
146 146
             bases=('promotions.handpickedproductlist',),
147 147
         ),
148 148
         migrations.CreateModel(
149 149
             name='PagePromotion',
150 150
             fields=[
151
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
151
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
152 152
                 ('object_id', models.PositiveIntegerField()),
153
-                ('position', models.CharField(help_text=b'Position on page', max_length=100, verbose_name='Position')),
154
-                ('display_order', models.PositiveIntegerField(default=0, verbose_name='Display Order')),
155
-                ('clicks', models.PositiveIntegerField(default=0, verbose_name='Clicks')),
156
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
157
-                ('page_url', oscar.models.fields.ExtendedURLField(max_length=128, verbose_name='Page URL', verify_exists=True, db_index=True)),
153
+                ('position', models.CharField(verbose_name='Position', help_text='Position on page', max_length=100)),
154
+                ('display_order', models.PositiveIntegerField(verbose_name='Display Order', default=0)),
155
+                ('clicks', models.PositiveIntegerField(verbose_name='Clicks', default=0)),
156
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
157
+                ('page_url', oscar.models.fields.ExtendedURLField(verbose_name='Page URL', verify_exists=True, db_index=True, max_length=128)),
158 158
                 ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
159 159
             ],
160 160
             options={
161
-                'ordering': [b'-clicks'],
162
-                'abstract': False,
163 161
                 'verbose_name': 'Page Promotion',
164 162
                 'verbose_name_plural': 'Page Promotions',
163
+                'ordering': ['-clicks'],
164
+                'abstract': False,
165 165
             },
166 166
             bases=(models.Model,),
167 167
         ),
168 168
         migrations.CreateModel(
169 169
             name='RawHTML',
170 170
             fields=[
171
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
172
-                ('name', models.CharField(max_length=128, verbose_name='Name')),
173
-                ('display_type', models.CharField(help_text='This can be used to have different types of HTML blocks (eg different widths)', max_length=128, verbose_name='Display type', blank=True)),
171
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
172
+                ('name', models.CharField(verbose_name='Name', max_length=128)),
173
+                ('display_type', models.CharField(verbose_name='Display type', blank=True, help_text='This can be used to have different types of HTML blocks (eg different widths)', max_length=128)),
174 174
                 ('body', models.TextField(verbose_name='HTML')),
175 175
                 ('date_created', models.DateTimeField(auto_now_add=True)),
176 176
             ],
@@ -183,8 +183,8 @@ class Migration(migrations.Migration):
183 183
         migrations.CreateModel(
184 184
             name='SingleProduct',
185 185
             fields=[
186
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
187
-                ('name', models.CharField(max_length=128, verbose_name='Name')),
186
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
187
+                ('name', models.CharField(verbose_name='Name', max_length=128)),
188 188
                 ('description', models.TextField(verbose_name='Description', blank=True)),
189 189
                 ('date_created', models.DateTimeField(auto_now_add=True)),
190 190
                 ('product', models.ForeignKey(to='catalogue.Product')),
@@ -198,9 +198,9 @@ class Migration(migrations.Migration):
198 198
         migrations.CreateModel(
199 199
             name='TabbedBlock',
200 200
             fields=[
201
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
202
-                ('name', models.CharField(max_length=255, verbose_name='Title')),
203
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
201
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
202
+                ('name', models.CharField(verbose_name='Title', max_length=255)),
203
+                ('date_created', models.DateTimeField(verbose_name='Date Created', auto_now_add=True)),
204 204
             ],
205 205
             options={
206 206
                 'verbose_name': 'Tabbed Block',

+ 13
- 6
oscar/apps/promotions/models.py Visa fil

@@ -1,5 +1,6 @@
1 1
 from django.db import models
2 2
 from django.conf import settings
3
+from django.utils.encoding import python_2_unicode_compatible
3 4
 from django.utils.translation import ugettext_lazy as _, pgettext_lazy
4 5
 from django.core.urlresolvers import reverse
5 6
 from django.contrib.contenttypes.models import ContentType
@@ -38,6 +39,7 @@ class LinkedPromotion(models.Model):
38 39
     record_click.alters_data = True
39 40
 
40 41
 
42
+@python_2_unicode_compatible
41 43
 class PagePromotion(LinkedPromotion):
42 44
     """
43 45
     A promotion embedded on a particular page.
@@ -45,7 +47,7 @@ class PagePromotion(LinkedPromotion):
45 47
     page_url = ExtendedURLField(
46 48
         _('Page URL'), max_length=128, db_index=True, verify_exists=True)
47 49
 
48
-    def __unicode__(self):
50
+    def __str__(self):
49 51
         return u"%s on %s" % (self.content_object, self.page_url)
50 52
 
51 53
     def get_link(self):
@@ -134,6 +136,7 @@ class AbstractPromotion(models.Model):
134 136
         return page_count + keyword_count
135 137
 
136 138
 
139
+@python_2_unicode_compatible
137 140
 class RawHTML(AbstractPromotion):
138 141
     """
139 142
     Simple promotion - just raw HTML
@@ -156,10 +159,11 @@ class RawHTML(AbstractPromotion):
156 159
         verbose_name = _('Raw HTML')
157 160
         verbose_name_plural = _('Raw HTML')
158 161
 
159
-    def __unicode__(self):
162
+    def __str__(self):
160 163
         return self.name
161 164
 
162 165
 
166
+@python_2_unicode_compatible
163 167
 class Image(AbstractPromotion):
164 168
     """
165 169
     An image promotion is simply a named image which has an optional
@@ -177,7 +181,7 @@ class Image(AbstractPromotion):
177 181
         max_length=255)
178 182
     date_created = models.DateTimeField(auto_now_add=True)
179 183
 
180
-    def __unicode__(self):
184
+    def __str__(self):
181 185
         return self.name
182 186
 
183 187
     class Meta:
@@ -186,6 +190,7 @@ class Image(AbstractPromotion):
186 190
         verbose_name_plural = _("Image")
187 191
 
188 192
 
193
+@python_2_unicode_compatible
189 194
 class MultiImage(AbstractPromotion):
190 195
     """
191 196
     A multi-image promotion is simply a collection of image promotions
@@ -201,7 +206,7 @@ class MultiImage(AbstractPromotion):
201 206
             "(You may need to create some first)."))
202 207
     date_created = models.DateTimeField(auto_now_add=True)
203 208
 
204
-    def __unicode__(self):
209
+    def __str__(self):
205 210
         return self.name
206 211
 
207 212
     class Meta:
@@ -210,6 +215,7 @@ class MultiImage(AbstractPromotion):
210 215
         verbose_name_plural = _("Multi Images")
211 216
 
212 217
 
218
+@python_2_unicode_compatible
213 219
 class SingleProduct(AbstractPromotion):
214 220
     _type = 'Single product'
215 221
     name = models.CharField(_("Name"), max_length=128)
@@ -217,7 +223,7 @@ class SingleProduct(AbstractPromotion):
217 223
     description = models.TextField(_("Description"), blank=True)
218 224
     date_created = models.DateTimeField(auto_now_add=True)
219 225
 
220
-    def __unicode__(self):
226
+    def __str__(self):
221 227
         return self.name
222 228
 
223 229
     def template_context(self, request):
@@ -229,6 +235,7 @@ class SingleProduct(AbstractPromotion):
229 235
         verbose_name_plural = _("Single product")
230 236
 
231 237
 
238
+@python_2_unicode_compatible
232 239
 class AbstractProductList(AbstractPromotion):
233 240
     """
234 241
     Abstract superclass for promotions which are essentially a list
@@ -248,7 +255,7 @@ class AbstractProductList(AbstractPromotion):
248 255
         verbose_name = _("Product list")
249 256
         verbose_name_plural = _("Product lists")
250 257
 
251
-    def __unicode__(self):
258
+    def __str__(self):
252 259
         return self.name
253 260
 
254 261
     def template_context(self, request):

+ 1
- 1
oscar/apps/search/facets.py Visa fil

@@ -1,6 +1,6 @@
1 1
 from django.conf import settings
2 2
 from purl import URL
3
-from six.moves import map
3
+from django.utils.six.moves import map
4 4
 
5 5
 
6 6
 def facet_data(request, form, results):  # noqa (too complex (10))

+ 5
- 2
oscar/apps/shipping/abstract_models.py Visa fil

@@ -2,6 +2,7 @@
2 2
 from decimal import Decimal as D
3 3
 
4 4
 from django.db import models
5
+from django.utils.encoding import python_2_unicode_compatible
5 6
 from django.utils.translation import ugettext_lazy as _
6 7
 from django.core.validators import MinValueValidator
7 8
 
@@ -11,6 +12,7 @@ from oscar.models.fields import AutoSlugField
11 12
 Scale = loading.get_class('shipping.scales', 'Scale')
12 13
 
13 14
 
15
+@python_2_unicode_compatible
14 16
 class AbstractBase(models.Model):
15 17
     """
16 18
     Implements the interface declared by shipping.base.Base
@@ -34,7 +36,7 @@ class AbstractBase(models.Model):
34 36
         verbose_name = _("Shipping Method")
35 37
         verbose_name_plural = _("Shipping Methods")
36 38
 
37
-    def __unicode__(self):
39
+    def __str__(self):
38 40
         return self.name
39 41
 
40 42
 
@@ -174,6 +176,7 @@ class AbstractWeightBased(AbstractBase):
174 176
             return None
175 177
 
176 178
 
179
+@python_2_unicode_compatible
177 180
 class AbstractWeightBand(models.Model):
178 181
     """
179 182
     Represents a weight band which are used by the WeightBasedShipping method.
@@ -208,5 +211,5 @@ class AbstractWeightBand(models.Model):
208 211
         verbose_name = _("Weight Band")
209 212
         verbose_name_plural = _("Weight Bands")
210 213
 
211
-    def __unicode__(self):
214
+    def __str__(self):
212 215
         return _('Charge for weights up to %s kg') % (self.upper_limit,)

+ 15
- 2
oscar/apps/shipping/methods.py Visa fil

@@ -44,6 +44,9 @@ class Base(object):
44 44
 
45 45
 
46 46
 class Free(Base):
47
+    """
48
+    This shipping method specifies that shipping is free.
49
+    """
47 50
     code = 'free-shipping'
48 51
     name = _('Free shipping')
49 52
 
@@ -65,6 +68,10 @@ class NoShippingRequired(Free):
65 68
 
66 69
 
67 70
 class FixedPrice(Base):
71
+    """
72
+    This shipping method indicates that shipping costs a fixed price and
73
+    requires no special calculation.
74
+    """
68 75
     code = 'fixed-price-shipping'
69 76
     name = _('Fixed price shipping')
70 77
 
@@ -88,8 +95,8 @@ class FixedPrice(Base):
88 95
 
89 96
 class OfferDiscount(Base):
90 97
     """
91
-    Wrapper class that applies a discount to an existing shipping method's
92
-    charges
98
+    Wrapper class that applies a discount to an existing shipping 
99
+    method's charges.
93 100
     """
94 101
     is_discounted = True
95 102
 
@@ -120,6 +127,9 @@ class OfferDiscount(Base):
120 127
 
121 128
 
122 129
 class TaxExclusiveOfferDiscount(OfferDiscount):
130
+    """
131
+    Wrapper class which extends OfferDiscount to be exclusive of tax.
132
+    """
123 133
 
124 134
     def calculate(self, basket):
125 135
         base_charge = self.method.calculate(basket)
@@ -135,6 +145,9 @@ class TaxExclusiveOfferDiscount(OfferDiscount):
135 145
 
136 146
 
137 147
 class TaxInclusiveOfferDiscount(OfferDiscount):
148
+    """
149
+    Wrapper class which extends OfferDiscount to be inclusive of tax.
150
+    """
138 151
 
139 152
     def calculate(self, basket):
140 153
         base_charge = self.method.calculate(basket)

+ 21
- 21
oscar/apps/shipping/migrations/0001_initial.py Visa fil

@@ -2,9 +2,9 @@
2 2
 from __future__ import unicode_literals
3 3
 
4 4
 from django.db import models, migrations
5
-import oscar.models.fields.autoslugfield
6 5
 from decimal import Decimal
7 6
 import django.core.validators
7
+import oscar.models.fields.autoslugfield
8 8
 
9 9
 
10 10
 class Migration(migrations.Migration):
@@ -17,53 +17,53 @@ class Migration(migrations.Migration):
17 17
         migrations.CreateModel(
18 18
             name='OrderAndItemCharges',
19 19
             fields=[
20
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
21
-                ('code', oscar.models.fields.autoslugfield.AutoSlugField(populate_from=b'name', editable=False, max_length=128, blank=True, unique=True, verbose_name='Slug')),
22
-                ('name', models.CharField(unique=True, max_length=128, verbose_name='Name')),
20
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
21
+                ('code', oscar.models.fields.autoslugfield.AutoSlugField(editable=False, verbose_name='Slug', blank=True, max_length=128, populate_from='name', unique=True)),
22
+                ('name', models.CharField(verbose_name='Name', unique=True, max_length=128)),
23 23
                 ('description', models.TextField(verbose_name='Description', blank=True)),
24
-                ('price_per_order', models.DecimalField(default=Decimal('0.00'), verbose_name='Price per order', max_digits=12, decimal_places=2)),
25
-                ('price_per_item', models.DecimalField(default=Decimal('0.00'), verbose_name='Price per item', max_digits=12, decimal_places=2)),
26
-                ('free_shipping_threshold', models.DecimalField(null=True, verbose_name='Free Shipping', max_digits=12, decimal_places=2, blank=True)),
27
-                ('countries', models.ManyToManyField(to='address.Country', null=True, verbose_name='Countries', blank=True)),
24
+                ('price_per_order', models.DecimalField(verbose_name='Price per order', max_digits=12, decimal_places=2, default=Decimal('0.00'))),
25
+                ('price_per_item', models.DecimalField(verbose_name='Price per item', max_digits=12, decimal_places=2, default=Decimal('0.00'))),
26
+                ('free_shipping_threshold', models.DecimalField(verbose_name='Free Shipping', max_digits=12, decimal_places=2, blank=True, null=True)),
27
+                ('countries', models.ManyToManyField(verbose_name='Countries', blank=True, to='address.Country', null=True)),
28 28
             ],
29 29
             options={
30
-                'ordering': [b'name'],
31
-                'abstract': False,
32 30
                 'verbose_name': 'Order and Item Charge',
33 31
                 'verbose_name_plural': 'Order and Item Charges',
32
+                'ordering': ['name'],
33
+                'abstract': False,
34 34
             },
35 35
             bases=(models.Model,),
36 36
         ),
37 37
         migrations.CreateModel(
38 38
             name='WeightBand',
39 39
             fields=[
40
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
41
-                ('upper_limit', models.DecimalField(help_text='Enter upper limit of this weight band in kg. The lower limit will be determined by the other weight bands.', verbose_name='Upper Limit', max_digits=12, decimal_places=3, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))])),
40
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
41
+                ('upper_limit', models.DecimalField(verbose_name='Upper Limit', max_digits=12, decimal_places=3, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))], help_text='Enter upper limit of this weight band in kg. The lower limit will be determined by the other weight bands.')),
42 42
                 ('charge', models.DecimalField(verbose_name='Charge', max_digits=12, decimal_places=2, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))])),
43 43
             ],
44 44
             options={
45
-                'ordering': [b'method', b'upper_limit'],
46
-                'abstract': False,
47 45
                 'verbose_name': 'Weight Band',
48 46
                 'verbose_name_plural': 'Weight Bands',
47
+                'ordering': ['method', 'upper_limit'],
48
+                'abstract': False,
49 49
             },
50 50
             bases=(models.Model,),
51 51
         ),
52 52
         migrations.CreateModel(
53 53
             name='WeightBased',
54 54
             fields=[
55
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
56
-                ('code', oscar.models.fields.autoslugfield.AutoSlugField(populate_from=b'name', editable=False, max_length=128, blank=True, unique=True, verbose_name='Slug')),
57
-                ('name', models.CharField(unique=True, max_length=128, verbose_name='Name')),
55
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
56
+                ('code', oscar.models.fields.autoslugfield.AutoSlugField(editable=False, verbose_name='Slug', blank=True, max_length=128, populate_from='name', unique=True)),
57
+                ('name', models.CharField(verbose_name='Name', unique=True, max_length=128)),
58 58
                 ('description', models.TextField(verbose_name='Description', blank=True)),
59
-                ('default_weight', models.DecimalField(decimal_places=3, default=Decimal('0.000'), max_digits=12, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))], help_text='Default product weight in kg when no weight attribute is defined', verbose_name='Default Weight')),
60
-                ('countries', models.ManyToManyField(to='address.Country', null=True, verbose_name='Countries', blank=True)),
59
+                ('default_weight', models.DecimalField(verbose_name='Default Weight', decimal_places=3, default=Decimal('0.000'), max_digits=12, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))], help_text='Default product weight in kg when no weight attribute is defined')),
60
+                ('countries', models.ManyToManyField(verbose_name='Countries', blank=True, to='address.Country', null=True)),
61 61
             ],
62 62
             options={
63
-                'ordering': [b'name'],
64
-                'abstract': False,
65 63
                 'verbose_name': 'Weight-based Shipping Method',
66 64
                 'verbose_name_plural': 'Weight-based Shipping Methods',
65
+                'ordering': ['name'],
66
+                'abstract': False,
67 67
             },
68 68
             bases=(models.Model,),
69 69
         ),

+ 5
- 2
oscar/apps/voucher/abstract_models.py Visa fil

@@ -2,11 +2,13 @@ from decimal import Decimal
2 2
 
3 3
 from django.core import exceptions
4 4
 from django.db import models
5
+from django.utils.encoding import python_2_unicode_compatible
5 6
 from django.utils.translation import ugettext_lazy as _
6 7
 from django.utils import timezone
7 8
 from oscar.core.compat import AUTH_USER_MODEL
8 9
 
9 10
 
11
+@python_2_unicode_compatible
10 12
 class AbstractVoucher(models.Model):
11 13
     """
12 14
     A voucher.  This is simply a link to a collection of offers.
@@ -57,7 +59,7 @@ class AbstractVoucher(models.Model):
57 59
         verbose_name = _("Voucher")
58 60
         verbose_name_plural = _("Vouchers")
59 61
 
60
-    def __unicode__(self):
62
+    def __str__(self):
61 63
         return self.name
62 64
 
63 65
     def clean(self):
@@ -129,6 +131,7 @@ class AbstractVoucher(models.Model):
129 131
         return self.offers.all()[0].benefit
130 132
 
131 133
 
134
+@python_2_unicode_compatible
132 135
 class AbstractVoucherApplication(models.Model):
133 136
     """
134 137
     For tracking how often a voucher has been used
@@ -150,7 +153,7 @@ class AbstractVoucherApplication(models.Model):
150 153
         verbose_name = _("Voucher Application")
151 154
         verbose_name_plural = _("Voucher Applications")
152 155
 
153
-    def __unicode__(self):
156
+    def __str__(self):
154 157
         return _("'%(voucher)s' used by '%(user)s'") % {
155 158
             'voucher': self.voucher,
156 159
             'user': self.user}

+ 13
- 13
oscar/apps/voucher/migrations/0001_initial.py Visa fil

@@ -18,39 +18,39 @@ class Migration(migrations.Migration):
18 18
         migrations.CreateModel(
19 19
             name='Voucher',
20 20
             fields=[
21
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
22
-                ('name', models.CharField(help_text='This will be shown in the checkout and basket once the voucher is entered', max_length=128, verbose_name='Name')),
23
-                ('code', models.CharField(help_text='Case insensitive / No spaces allowed', unique=True, max_length=128, verbose_name='Code', db_index=True)),
24
-                ('usage', models.CharField(default=b'Multi-use', max_length=128, verbose_name='Usage', choices=[(b'Single use', 'Can be used once by one customer'), (b'Multi-use', 'Can be used multiple times by multiple customers'), (b'Once per customer', 'Can only be used once per customer')])),
21
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
22
+                ('name', models.CharField(verbose_name='Name', help_text='This will be shown in the checkout and basket once the voucher is entered', max_length=128)),
23
+                ('code', models.CharField(verbose_name='Code', unique=True, help_text='Case insensitive / No spaces allowed', db_index=True, max_length=128)),
24
+                ('usage', models.CharField(verbose_name='Usage', choices=[('Single use', 'Can be used once by one customer'), ('Multi-use', 'Can be used multiple times by multiple customers'), ('Once per customer', 'Can only be used once per customer')], default='Multi-use', max_length=128)),
25 25
                 ('start_datetime', models.DateTimeField(verbose_name='Start datetime')),
26 26
                 ('end_datetime', models.DateTimeField(verbose_name='End datetime')),
27
-                ('num_basket_additions', models.PositiveIntegerField(default=0, verbose_name='Times added to basket')),
28
-                ('num_orders', models.PositiveIntegerField(default=0, verbose_name='Times on orders')),
29
-                ('total_discount', models.DecimalField(default=Decimal('0.00'), verbose_name='Total discount', max_digits=12, decimal_places=2)),
27
+                ('num_basket_additions', models.PositiveIntegerField(verbose_name='Times added to basket', default=0)),
28
+                ('num_orders', models.PositiveIntegerField(verbose_name='Times on orders', default=0)),
29
+                ('total_discount', models.DecimalField(verbose_name='Total discount', max_digits=12, decimal_places=2, default=Decimal('0.00'))),
30 30
                 ('date_created', models.DateField(auto_now_add=True)),
31
-                ('offers', models.ManyToManyField(to='offer.ConditionalOffer', verbose_name='Offers')),
31
+                ('offers', models.ManyToManyField(verbose_name='Offers', to='offer.ConditionalOffer')),
32 32
             ],
33 33
             options={
34
-                'abstract': False,
35
-                'get_latest_by': b'date_created',
36 34
                 'verbose_name': 'Voucher',
37 35
                 'verbose_name_plural': 'Vouchers',
36
+                'abstract': False,
37
+                'get_latest_by': 'date_created',
38 38
             },
39 39
             bases=(models.Model,),
40 40
         ),
41 41
         migrations.CreateModel(
42 42
             name='VoucherApplication',
43 43
             fields=[
44
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
45
-                ('date_created', models.DateField(auto_now_add=True, verbose_name='Date Created')),
44
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
45
+                ('date_created', models.DateField(verbose_name='Date Created', auto_now_add=True)),
46 46
                 ('order', models.ForeignKey(verbose_name='Order', to='order.Order')),
47 47
                 ('user', models.ForeignKey(verbose_name='User', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
48 48
                 ('voucher', models.ForeignKey(verbose_name='Voucher', to='voucher.Voucher')),
49 49
             ],
50 50
             options={
51
-                'abstract': False,
52 51
                 'verbose_name': 'Voucher Application',
53 52
                 'verbose_name_plural': 'Voucher Applications',
53
+                'abstract': False,
54 54
             },
55 55
             bases=(models.Model,),
56 56
         ),

+ 6
- 3
oscar/apps/wishlists/abstract_models.py Visa fil

@@ -1,14 +1,16 @@
1 1
 import hashlib
2 2
 import random
3
-import six
3
+from django.utils import six
4 4
 
5 5
 from django.db import models
6
+from django.utils.encoding import python_2_unicode_compatible
6 7
 from django.utils.translation import ugettext_lazy as _, pgettext_lazy
7 8
 from django.core.urlresolvers import reverse
8 9
 
9 10
 from oscar.core.compat import AUTH_USER_MODEL
10 11
 
11 12
 
13
+@python_2_unicode_compatible
12 14
 class AbstractWishList(models.Model):
13 15
     """
14 16
     Represents a user's wish lists of products.
@@ -48,7 +50,7 @@ class AbstractWishList(models.Model):
48 50
     date_created = models.DateTimeField(
49 51
         _('Date created'), auto_now_add=True, editable=False)
50 52
 
51
-    def __unicode__(self):
53
+    def __str__(self):
52 54
         return u"%s's Wish List '%s'" % (self.owner, self.name)
53 55
 
54 56
     def save(self, *args, **kwargs):
@@ -101,6 +103,7 @@ class AbstractWishList(models.Model):
101 103
             line.save()
102 104
 
103 105
 
106
+@python_2_unicode_compatible
104 107
 class AbstractLine(models.Model):
105 108
     """
106 109
     One entry in a wish list. Similar to order lines or basket lines.
@@ -116,7 +119,7 @@ class AbstractLine(models.Model):
116 119
     title = models.CharField(
117 120
         pgettext_lazy(u"Product title", u"Title"), max_length=255)
118 121
 
119
-    def __unicode__(self):
122
+    def __str__(self):
120 123
         return u'%sx %s on %s' % (self.quantity, self.title,
121 124
                                   self.wishlist.name)
122 125
 

+ 13
- 13
oscar/apps/wishlists/migrations/0001_initial.py Visa fil

@@ -17,31 +17,31 @@ class Migration(migrations.Migration):
17 17
         migrations.CreateModel(
18 18
             name='Line',
19 19
             fields=[
20
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
21
-                ('quantity', models.PositiveIntegerField(default=1, verbose_name='Quantity')),
22
-                ('title', models.CharField(max_length=255, verbose_name='Title')),
23
-                ('product', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='Product', blank=True, to='catalogue.Product', null=True)),
20
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
21
+                ('quantity', models.PositiveIntegerField(verbose_name='Quantity', default=1)),
22
+                ('title', models.CharField(verbose_name='Title', max_length=255)),
23
+                ('product', models.ForeignKey(verbose_name='Product', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='catalogue.Product', null=True)),
24 24
             ],
25 25
             options={
26
-                'abstract': False,
27 26
                 'verbose_name': 'Wish list line',
27
+                'abstract': False,
28 28
             },
29 29
             bases=(models.Model,),
30 30
         ),
31 31
         migrations.CreateModel(
32 32
             name='WishList',
33 33
             fields=[
34
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
35
-                ('name', models.CharField(default='Default', max_length=255, verbose_name='Name')),
36
-                ('key', models.CharField(verbose_name='Key', unique=True, max_length=6, editable=False, db_index=True)),
37
-                ('visibility', models.CharField(default=b'Private', max_length=20, verbose_name='Visibility', choices=[(b'Private', 'Private - Only the owner can see the wish list'), (b'Shared', 'Shared - Only the owner and people with access to the obfuscated link can see the wish list'), (b'Public', 'Public - Everybody can see the wish list')])),
38
-                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
34
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
35
+                ('name', models.CharField(verbose_name='Name', default='Default', max_length=255)),
36
+                ('key', models.CharField(editable=False, verbose_name='Key', unique=True, db_index=True, max_length=6)),
37
+                ('visibility', models.CharField(verbose_name='Visibility', choices=[('Private', 'Private - Only the owner can see the wish list'), ('Shared', 'Shared - Only the owner and people with access to the obfuscated link can see the wish list'), ('Public', 'Public - Everybody can see the wish list')], default='Private', max_length=20)),
38
+                ('date_created', models.DateTimeField(verbose_name='Date created', auto_now_add=True)),
39 39
                 ('owner', models.ForeignKey(verbose_name='Owner', to=settings.AUTH_USER_MODEL)),
40 40
             ],
41 41
             options={
42
-                'ordering': (b'owner', b'date_created'),
43
-                'abstract': False,
44 42
                 'verbose_name': 'Wish List',
43
+                'ordering': ('owner', 'date_created'),
44
+                'abstract': False,
45 45
             },
46 46
             bases=(models.Model,),
47 47
         ),
@@ -53,6 +53,6 @@ class Migration(migrations.Migration):
53 53
         ),
54 54
         migrations.AlterUniqueTogether(
55 55
             name='line',
56
-            unique_together=set([(b'wishlist', b'product')]),
56
+            unique_together=set([('wishlist', 'product')]),
57 57
         ),
58 58
     ]

+ 1
- 1
oscar/core/ajax.py Visa fil

@@ -1,4 +1,4 @@
1
-import six
1
+from django.utils import six
2 2
 from django.contrib import messages
3 3
 
4 4
 

+ 1
- 3
oscar/core/application.py Visa fil

@@ -1,5 +1,3 @@
1
-import six
2
-
3 1
 from oscar.core.loading import feature_hidden
4 2
 from oscar.views.decorators import permissions_required
5 3
 
@@ -26,7 +24,7 @@ class Application(object):
26 24
     def __init__(self, app_name=None, **kwargs):
27 25
         self.app_name = app_name
28 26
         # Set all kwargs as object attributes
29
-        for key, value in six.iteritems(kwargs):
27
+        for key, value in kwargs.items():
30 28
             setattr(self, key, value)
31 29
 
32 30
     def get_urls(self):

+ 1
- 1
oscar/core/compat.py Visa fil

@@ -1,4 +1,4 @@
1
-import six
1
+from django.utils import six
2 2
 
3 3
 from django.conf import settings
4 4
 from django.contrib.auth import get_user_model as django_get_user_model

+ 1
- 1
oscar/core/context_processors.py Visa fil

@@ -2,7 +2,7 @@ import oscar
2 2
 import re
3 3
 import platform
4 4
 import django
5
-from six.moves.urllib import parse
5
+from django.utils.six.moves.urllib import parse
6 6
 from django.conf import settings
7 7
 from django.utils.safestring import mark_safe
8 8
 

+ 14
- 5
oscar/core/loading.py Visa fil

@@ -4,7 +4,7 @@ from importlib import import_module
4 4
 
5 5
 import django
6 6
 from django.conf import settings
7
-from django.utils import six as django_six
7
+from django.utils import six
8 8
 
9 9
 from oscar.core.exceptions import (ModuleNotFoundError, ClassNotFoundError,
10 10
                                    AppNotFoundError)
@@ -23,7 +23,7 @@ def import_string(dotted_path):
23 23
         module_path, class_name = dotted_path.rsplit('.', 1)
24 24
     except ValueError:
25 25
         msg = "%s doesn't look like a module path" % dotted_path
26
-        django_six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
26
+        six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
27 27
 
28 28
     module = import_module(module_path)
29 29
 
@@ -32,12 +32,12 @@ def import_string(dotted_path):
32 32
     except AttributeError:
33 33
         msg = 'Module "%s" does not define a "%s" attribute/class' % (
34 34
             dotted_path, class_name)
35
-        django_six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
35
+        six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
36 36
     try:
37 37
         module_path, class_name = dotted_path.rsplit('.', 1)
38 38
     except ValueError:
39 39
         msg = "%s doesn't look like a module path" % dotted_path
40
-        django_six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
40
+        six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
41 41
 
42 42
     module = __import__(module_path, fromlist=[class_name])
43 43
 
@@ -46,7 +46,7 @@ def import_string(dotted_path):
46 46
     except AttributeError:
47 47
         msg = 'Module "%s" does not define a "%s" attribute/class' % (
48 48
             dotted_path, class_name)
49
-        django_six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
49
+        six.reraise(ImportError, ImportError(msg), sys.exc_info()[2])
50 50
 
51 51
 
52 52
 def get_class(module_label, classname):
@@ -278,6 +278,15 @@ if django.VERSION < (1, 7):
278 278
         This is merely a thin wrapper around Django's get_model function.
279 279
         Raises LookupError if model isn't found.
280 280
         """
281
+
282
+        # The snippet below is not useful in production, but helpful to
283
+        # investigate circular import issues
284
+        # from django.db.models.loading import app_cache_ready
285
+        # if not app_cache_ready():
286
+        #     print(
287
+        #         "%s.%s accessed before app cache is fully populated!" %
288
+        #         (app_label, model_name))
289
+
281 290
         model = django_get_model(app_label, model_name, *args, **kwargs)
282 291
         if model is None:
283 292
             raise LookupError(

+ 5
- 6
oscar/core/phonenumber.py Visa fil

@@ -26,16 +26,18 @@
26 26
 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 27
 # OTHER DEALINGS IN THE SOFTWARE.
28 28
 
29
-import six
29
+from django.utils import six
30 30
 
31 31
 from django.core import validators
32 32
 from django.conf import settings
33 33
 from django.core.exceptions import ValidationError
34 34
 from django.utils.translation import ugettext_lazy as _
35
+from django.utils.encoding import python_2_unicode_compatible
35 36
 
36 37
 import phonenumbers
37 38
 
38 39
 
40
+@python_2_unicode_compatible
39 41
 class PhoneNumber(phonenumbers.phonenumber.PhoneNumber):
40 42
     """
41 43
     A extended version of phonenumbers.phonenumber.PhoneNumber that provides
@@ -58,7 +60,7 @@ class PhoneNumber(phonenumbers.phonenumber.PhoneNumber):
58 60
                            keep_raw_input=True, numobj=phone_number_obj)
59 61
         return phone_number_obj
60 62
 
61
-    def __unicode__(self):
63
+    def __str__(self):
62 64
         format_string = getattr(
63 65
             settings, 'PHONENUMBER_DEFAULT_FORMAT', 'INTERNATIONAL')
64 66
         fmt = self.format_map[format_string]
@@ -66,9 +68,6 @@ class PhoneNumber(phonenumbers.phonenumber.PhoneNumber):
66 68
             return self.format_as(fmt)
67 69
         return self.raw_input
68 70
 
69
-    def __str__(self):
70
-        return str(self.__unicode__())
71
-
72 71
     def is_valid(self):
73 72
         """
74 73
         checks whether the number supplied is actually valid
@@ -98,7 +97,7 @@ class PhoneNumber(phonenumbers.phonenumber.PhoneNumber):
98 97
         return self.format_as(phonenumbers.PhoneNumberFormat.RFC3966)
99 98
 
100 99
     def __len__(self):
101
-        return len(self.__unicode__())
100
+        return len(six.text_type(self))
102 101
 
103 102
     def __eq__(self, other):
104 103
         if type(other) == PhoneNumber:

+ 8
- 0
oscar/core/prices.py Visa fil

@@ -46,3 +46,11 @@ class Price(object):
46 46
                 self.incl_tax, self.tax)
47 47
         return "%s(currency=%r, excl_tax=%r)" % (
48 48
             self.__class__.__name__, self.currency, self.excl_tax)
49
+
50
+    def __eq__(self, other):
51
+        """
52
+        Two price objects are equal if currency, price.excl_tax and tax match.
53
+        """
54
+        return (self.currency == other.currency and
55
+                self.excl_tax == other.excl_tax and
56
+                self.incl_tax == other.incl_tax)

+ 32
- 1
oscar/core/utils.py Visa fil

@@ -1,7 +1,9 @@
1 1
 from __future__ import absolute_import  # for logging import below
2 2
 import logging
3
-import six
4 3
 
4
+from django.shortcuts import redirect, resolve_url
5
+from django.utils import six
6
+from django.utils.http import is_safe_url
5 7
 from django.utils.timezone import get_current_timezone, is_naive, make_aware
6 8
 from django.conf import settings
7 9
 from django.template.defaultfilters import (date as date_filter,
@@ -81,3 +83,32 @@ def format_datetime(dt, format=None):
81 83
     else:
82 84
         localtime = dt.astimezone(get_current_timezone())
83 85
     return date_filter(localtime, format)
86
+
87
+
88
+def safe_referrer(meta, default):
89
+    """
90
+    Takes request.META and a default URL. Returns HTTP_REFERER if it's safe
91
+    to use and set, and the default URL otherwise.
92
+
93
+    The default URL can be a model with get_absolute_url defined, a urlname
94
+    or a regular URL
95
+    """
96
+    referrer = meta.get('HTTP_REFERER')
97
+    if referrer and is_safe_url(referrer):
98
+        return referrer
99
+    if default:
100
+        # try to resolve
101
+        return resolve_url(default)
102
+    else:
103
+        # Allow passing in '' and None as default
104
+        return default
105
+
106
+
107
+def redirect_to_referrer(meta, default):
108
+    """
109
+    Takes request.META and a default URL to redirect to.
110
+
111
+    Returns a HttpResponseRedirect to HTTP_REFERER if it exists and is a safe
112
+    URL; to the default URL otherwise.
113
+    """
114
+    return redirect(safe_referrer(meta, default))

+ 0
- 3
oscar/defaults.py Visa fil

@@ -70,9 +70,6 @@ OSCAR_EAGER_ALERTS = True
70 70
 OSCAR_SEND_REGISTRATION_EMAIL = True
71 71
 OSCAR_FROM_EMAIL = 'oscar@example.com'
72 72
 
73
-# Offers
74
-OSCAR_OFFER_BLACKLIST_PRODUCT = None
75
-
76 73
 # Slug handling
77 74
 OSCAR_SLUG_FUNCTION = 'oscar.core.utils.default_slugifier'
78 75
 OSCAR_SLUG_MAP = {}

+ 5
- 5
oscar/forms/widgets.py Visa fil

@@ -1,7 +1,7 @@
1 1
 import re
2
-import six
3
-from six.moves import filter
4
-from six.moves import map
2
+from django.utils import six
3
+from django.utils.six.moves import filter
4
+from django.utils.six.moves import map
5 5
 
6 6
 import django
7 7
 from django import forms
@@ -73,7 +73,7 @@ def datetime_format_to_js_date_format(format):
73 73
         '%d': 'dd',
74 74
         '%H:%M': '',
75 75
     }
76
-    for search, replace in six.iteritems(replacements):
76
+    for search, replace in replacements.items():
77 77
         converted = converted.replace(search, replace)
78 78
     return converted.strip()
79 79
 
@@ -91,7 +91,7 @@ def datetime_format_to_js_time_format(format):
91 91
         '%H': 'HH',
92 92
         '%M': 'mm',
93 93
     }
94
-    for search, replace in six.iteritems(replacements):
94
+    for search, replace in replacements.items():
95 95
         converted = converted.replace(search, replace)
96 96
 
97 97
     converted = re.sub('[-/][^%]', '', converted)

+ 3
- 1
oscar/management/commands/oscar_fork_app.py Visa fil

@@ -1,5 +1,7 @@
1 1
 import logging
2 2
 
3
+from django.utils import six
4
+
3 5
 from django.core.management.base import BaseCommand, CommandError
4 6
 
5 7
 from oscar.core import customisation
@@ -28,4 +30,4 @@ class Command(BaseCommand):
28 30
         try:
29 31
             customisation.fork_app(app_label, folder_path, logger)
30 32
         except Exception as e:
31
-            raise CommandError(e)
33
+            raise CommandError(six.text_type(e))

+ 0
- 2
oscar/management/commands/oscar_fork_statics.py Visa fil

@@ -1,5 +1,3 @@
1
-from __future__ import print_function
2
-from __future__ import print_function
3 1
 import logging
4 2
 import os
5 3
 import shutil

+ 0
- 1
oscar/management/commands/oscar_generate_email_content.py Visa fil

@@ -1,4 +1,3 @@
1
-from __future__ import print_function
2 1
 from django.core.management.base import BaseCommand, CommandError
3 2
 from oscar.core.loading import get_model
4 3
 

+ 3
- 4
oscar/models/fields/__init__.py Visa fil

@@ -1,9 +1,8 @@
1
-import six
2 1
 
3 2
 from django.core.exceptions import ImproperlyConfigured
4 3
 from django.db.models.fields import CharField, DecimalField, Field
5 4
 from django.db.models import SubfieldBase
6
-from django.utils import six as django_six
5
+from django.utils import six
7 6
 from django.utils.translation import ugettext_lazy as _
8 7
 from django.core.validators import MaxLengthValidator
9 8
 
@@ -84,7 +83,7 @@ class PositiveDecimalField(DecimalField):
84 83
         return super(PositiveDecimalField, self).formfield(min_value=0)
85 84
 
86 85
 
87
-class UppercaseCharField(django_six.with_metaclass(SubfieldBase, CharField)):
86
+class UppercaseCharField(six.with_metaclass(SubfieldBase, CharField)):
88 87
     """
89 88
     A simple subclass of ``django.db.models.fields.CharField`` that
90 89
     restricts all text to be uppercase.
@@ -101,7 +100,7 @@ class UppercaseCharField(django_six.with_metaclass(SubfieldBase, CharField)):
101 100
             return val
102 101
 
103 102
 
104
-class NullCharField(django_six.with_metaclass(SubfieldBase, CharField)):
103
+class NullCharField(six.with_metaclass(SubfieldBase, CharField)):
105 104
     """
106 105
     CharField that stores '' as None and returns None as ''
107 106
     Useful when using unique=True and forms. Implies null==blank==True.

+ 1
- 1
oscar/models/fields/autoslugfield.py Visa fil

@@ -25,7 +25,7 @@ THE SOFTWARE.
25 25
 """
26 26
 
27 27
 import re
28
-import six
28
+from django.utils import six
29 29
 
30 30
 from django.db.models import SlugField
31 31
 

+ 0
- 2
oscar/profiling/decorators.py Visa fil

@@ -1,5 +1,3 @@
1
-from __future__ import print_function
2
-from __future__ import print_function
3 1
 import cProfile
4 2
 import pstats
5 3
 import time

+ 0
- 2
oscar/profiling/middleware.py Visa fil

@@ -1,5 +1,3 @@
1
-from __future__ import print_function
2
-from __future__ import print_function
3 1
 import sys
4 2
 import tempfile
5 3
 import hotshot

+ 6
- 0
oscar/static/oscar/css/dashboard.css Visa fil

@@ -3023,6 +3023,12 @@ input[type=text].hasDatepicker,
3023 3023
 .table {
3024 3024
   background-color: #ffffff;
3025 3025
 }
3026
+.table thead tr th.asc i:before {
3027
+  content: "\f077";
3028
+}
3029
+.table thead tr th.desc i:before {
3030
+  content: "\f078";
3031
+}
3026 3032
 .table-striped tbody tr:nth-child(odd) td,
3027 3033
 .table-striped tbody tr:nth-child(odd) th {
3028 3034
   background-color: #efefef;

+ 0
- 6
oscar/static/oscar/css/styles.css Visa fil

@@ -7569,25 +7569,21 @@ h1,
7569 7569
 .h1 {
7570 7570
   font-size: 30.099999999999998px;
7571 7571
   line-height: 40px;
7572
-  font-weight: bold;
7573 7572
 }
7574 7573
 h2,
7575 7574
 .h2 {
7576 7575
   font-size: 24.5px;
7577 7576
   line-height: 40px;
7578
-  font-weight: bold;
7579 7577
 }
7580 7578
 h3,
7581 7579
 .h3 {
7582 7580
   font-size: 17.5px;
7583 7581
   line-height: 24px;
7584
-  font-weight: bold;
7585 7582
 }
7586 7583
 h4,
7587 7584
 .h4 {
7588 7585
   font-size: 16.099999999999998px;
7589 7586
   line-height: 24px;
7590
-  font-weight: bold;
7591 7587
 }
7592 7588
 h5,
7593 7589
 .h5 {
@@ -7760,7 +7756,6 @@ ul.row-fluid {
7760 7756
 .product_pod .price_color {
7761 7757
   font-size: 16.099999999999998px;
7762 7758
   line-height: 24px;
7763
-  font-weight: bold;
7764 7759
 }
7765 7760
 .product_pod .availability,
7766 7761
 .product_pod .price_color {
@@ -7940,7 +7935,6 @@ a:hover .thumbnail {
7940 7935
 .sidebar .promotion_single h2 {
7941 7936
   font-size: 17.5px;
7942 7937
   line-height: 24px;
7943
-  font-weight: bold;
7944 7938
 }
7945 7939
 .sidebar .promotion_single h3 {
7946 7940
   font-size: 14px;

+ 2
- 1
oscar/static/oscar/js/oscar/ui.js Visa fil

@@ -115,7 +115,7 @@ var oscar = (function(o, $) {
115 115
         },
116 116
         initNav: function() {
117 117
             // Initial navigation for desktop
118
-            var $sidebar = $('aside.span3'), 
118
+            var $sidebar = $('aside.span3'),
119 119
                 $browse = $('[data-navigation="dropdown-menu"]'),
120 120
                 $browseOpen = $browse.parent().find('> a[data-toggle]');
121 121
             // Set width of nav dropdown to be same as sidebar
@@ -212,6 +212,7 @@ var oscar = (function(o, $) {
212 212
         showVoucherForm: function() {
213 213
             $('#voucher_form_container').show();
214 214
             $('#voucher_form_link').hide();
215
+            $('#id_code').focus();
215 216
         },
216 217
         hideVoucherForm: function() {
217 218
             $('#voucher_form_container').hide();

+ 6
- 0
oscar/static/oscar/less/dashboard.less Visa fil

@@ -229,6 +229,12 @@ input[type=text].hasDatepicker,
229 229
 .table {
230 230
   background-color: @white;
231 231
 }
232
+.table thead tr th.asc i:before {
233
+  content: "\f077";
234
+}
235
+.table thead tr th.desc i:before {
236
+  content: "\f078";
237
+}
232 238
 .table-striped tbody tr:nth-child(odd) td,
233 239
 .table-striped tbody tr:nth-child(odd) th {
234 240
   background-color: #efefef;

+ 4
- 4
oscar/templates/oscar/500.html Visa fil

@@ -2,14 +2,14 @@
2 2
 {% load i18n %}
3 3
 
4 4
 {% block title %}
5
-{% trans 'Server error!' %} | {{ block.super }}
5
+    {% trans 'Server error!' %} | {{ block.super }}
6 6
 {% endblock %}
7 7
 
8 8
 {% block error_heading %}
9
-{% trans "Server error!" %}
9
+    {% trans "Server error!" %}
10 10
 {% endblock %}
11 11
 
12 12
 {% block error_message %}
13
-<p>{% trans "We're sorry but something went terribly wrong." %}</p>
14
-<p>{% trans "This incident has been logged and will be addressed by an engineer as soon as possible." %}</p>
13
+    <p>{% trans "We're sorry but something went terribly wrong and we've been unable to generate the page you requested." %}</p>
14
+    <p>{% trans "This incident has been logged and will be addressed by an engineer as soon as possible." %}</p>
15 15
 {% endblock %}

+ 17
- 14
oscar/templates/oscar/basket/partials/basket_content.html Visa fil

@@ -108,22 +108,25 @@
108 108
 
109 109
     <div class="row-fluid">
110 110
         {% block vouchers %}
111
-            <div class="span6">
112
-                <div class="sub-header">
113
-                    <h2>{% trans "Voucher/promo code" %}</h2>
114
-                </div>
115
-                <p id="voucher_form_link"><a href="#voucher" class="btn btn-full">{% trans "I have a voucher code..." %}</a></p>
111
+            {# Hide the entire section if a custom BasketView doesn't pass in a voucher form #}
112
+            {% if voucher_form %}
113
+                <div class="span6">
114
+                    <div class="sub-header">
115
+                        <h2>{% trans "Voucher/promo code" %}</h2>
116
+                    </div>
117
+                    <p id="voucher_form_link"><a href="#voucher" class="btn btn-full">{% trans "I have a voucher code..." %}</a></p>
116 118
 
117
-                <div id="voucher_form_container" style="display:none">
118
-                    <h3>{% trans "Voucher code" %}</h3>
119
-                    <form id="voucher_form" action="{% url 'basket:vouchers-add' %}" method="post">
120
-                        {% csrf_token %}
121
-                        {% include "partials/form_fields.html" with form=voucher_form %}
122
-                        <button type="submit" class="btn btn-info">{% trans "Add voucher" %}</button>
123
-                        {% trans "or" %} <a href="#" id="voucher_form_cancel">{% trans "cancel" %}</a>
124
-                    </form>
119
+                    <div id="voucher_form_container" style="display:none">
120
+                        <h3>{% trans "Voucher code" %}</h3>
121
+                        <form id="voucher_form" action="{% url 'basket:vouchers-add' %}" method="post">
122
+                            {% csrf_token %}
123
+                            {% include "partials/form_fields.html" with form=voucher_form %}
124
+                            <button type="submit" class="btn btn-info">{% trans "Add voucher" %}</button>
125
+                            {% trans "or" %} <a href="#" id="voucher_form_cancel">{% trans "cancel" %}</a>
126
+                        </form>
127
+                    </div>
125 128
                 </div>
126
-            </div>
129
+            {% endif %}
127 130
         {% endblock vouchers %}
128 131
 
129 132
         {% block baskettotals %}

+ 4
- 59
oscar/templates/oscar/dashboard/catalogue/category_list.html Visa fil

@@ -1,6 +1,7 @@
1 1
 {% extends 'dashboard/layout.html' %}
2 2
 {% load category_tags %}
3 3
 {% load i18n %}
4
+{% load render_table from django_tables2 %}
4 5
 
5 6
 {% block body_class %}{{ block.super }} catalogue{% endblock %}
6 7
 
@@ -38,65 +39,9 @@
38 39
         </p>
39 40
     </div>
40 41
 
41
-    {% if child_categories %}
42
-        <table class="table table-striped table-bordered table-hover">
43
-            <caption><i class="icon-sitemap icon-large"></i>{% trans 'Categories' %}</caption>
44
-            <thead>
45
-                <tr>
46
-                    <th>{% trans "Name" %}</th>
47
-                    <th>{% trans "Description" %}</th>
48
-                    <th>{% trans "Number of child categories" %}</th>
49
-                    <th></th>
50
-                </tr>
51
-            </thead>
52
-            <tbody>
53
-                {% for category in child_categories %}
54
-                    <tr>
55
-                        <td><a href="{% url 'dashboard:catalogue-category-update' category.id %}">{{ category.name }}</a></td>
56
-                        <td>{{ category.description|default:""|striptags|cut:"&nbsp;"|truncatewords:6 }}</td>
57
-                        <td><a href="{% url 'dashboard:catalogue-category-detail-list' pk=category.pk %}">{{ category.get_num_children }}</a></td>
58
-                        <td>
59
-                            <div class="btn-toolbar">
60
-                                <div class="btn-group">
61
-                                    <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
62
-                                        {% trans "Actions" %}
63
-                                        <span class="caret"></span>
64
-                                    </a>
65
-                                    <ul class="nav dropdown-menu pull-right">
66
-                                        <li>
67
-                                            <a href="{% url 'dashboard:catalogue-category-update' pk=category.id %}">
68
-                                                {% trans "Edit category" %}
69
-                                            </a>
70
-                                        </li>
71
-                                        <li>
72
-                                            <a href="{% url 'dashboard:catalogue-category-create-child' parent=category.pk %}">
73
-                                                {% trans "Add child category" %}
74
-                                            </a>
75
-                                        </li>
76
-                                        <li{% if not category.has_children %} class="disabled"{% endif %}>
77
-                                            <a href="{% url 'dashboard:catalogue-category-detail-list' pk=category.pk %}">
78
-                                                {% trans "Edit children" %}
79
-                                            </a>
80
-                                        </li>
81
-                                        <li>
82
-                                            <a href="{{ category.get_absolute_url }}">
83
-                                                {% trans "View on site" %}
84
-                                            </a>
85
-                                        </li>
86
-                                        <li>
87
-                                            <a href="{% url 'dashboard:catalogue-category-delete' pk=category.id %}">
88
-                                                {% trans "Delete" %}
89
-                                            </a>
90
-                                        </li>
91
-                                    </ul>
92
-                                </div>
93
-                            </div>
94
-                        </td>
95
-                    </tr>
96
-                {% endfor %}
97
-            </tbody>
98
-        </table>
99
-    {% else %}
42
+    {% render_table categories %}
43
+
44
+    {% if not child_categories %}
100 45
         <p>{% trans "There are no categories." %}</p>
101 46
     {% endif %}
102 47
 {% endblock dashboard_content %}

+ 37
- 0
oscar/templates/oscar/dashboard/catalogue/category_row_actions.html Visa fil

@@ -0,0 +1,37 @@
1
+{% load django_tables2 %}
2
+{% load i18n %}
3
+<div class="btn-toolbar">
4
+    <div class="btn-group">
5
+        <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
6
+            {% trans "Actions" %}
7
+            <span class="caret"></span>
8
+        </a>
9
+        <ul class="nav dropdown-menu pull-right">
10
+            <li>
11
+                <a href="{% url 'dashboard:catalogue-category-update' pk=record.id %}">
12
+                    {% trans "Edit category" %}
13
+                </a>
14
+            </li>
15
+            <li>
16
+                <a href="{% url 'dashboard:catalogue-category-create-child' parent=record.pk %}">
17
+                    {% trans "Add child category" %}
18
+                </a>
19
+            </li>
20
+            <li{% if not record.has_children %} class="disabled"{% endif %}>
21
+                <a href="{% url 'dashboard:catalogue-category-detail-list' pk=record.pk %}">
22
+                    {% trans "Edit children" %}
23
+                </a>
24
+            </li>
25
+            <li>
26
+                <a href="{{ record.get_absolute_url }}">
27
+                    {% trans "View on site" %}
28
+                </a>
29
+            </li>
30
+            <li>
31
+                <a href="{% url 'dashboard:catalogue-category-delete' pk=record.id %}">
32
+                    {% trans "Delete" %}
33
+                </a>
34
+            </li>
35
+        </ul>
36
+    </div>
37
+</div>

+ 2
- 1
oscar/templates/oscar/dashboard/catalogue/messages/product_saved.html Visa fil

@@ -1,4 +1,5 @@
1 1
 {% load i18n %}
2
+{% load django_tables2 %}
2 3
 
3 4
 <p>
4 5
 {% with name=product.title parent_name=product.parent.title %}
@@ -44,6 +45,6 @@
44 45
 </p>
45 46
 
46 47
 <p>
47
-    <a href="{% url 'dashboard:catalogue-product' pk=product.pk %}" class="btn btn-success">{% trans "Edit again" %}</a>
48
+    <a href="{% url 'dashboard:catalogue-product' pk=product.pk %}{% querystring %}" class="btn btn-success">{% trans "Edit again" %}</a>
48 49
     <a href="{{ product.get_absolute_url }}" class="btn btn-success">{% trans "View it on the site" %}</a>
49 50
 </p>

+ 2
- 93
oscar/templates/oscar/dashboard/catalogue/product_list.html Visa fil

@@ -3,6 +3,7 @@
3 3
 {% load thumbnail %}
4 4
 {% load staticfiles %}
5 5
 {% load sorting_tags %}
6
+{% load render_table from django_tables2 %}
6 7
 
7 8
 {% block body_class %}{{ block.super }} catalogue{% endblock %}
8 9
 
@@ -69,101 +70,9 @@
69 70
                     <li{% if 'recently_edited' in request.GET %} class="active"{% endif %}> <a href="?recently_edited=1">{% trans "Recently edited" %}</a> </li>
70 71
                 </ul>
71 72
             {% endblock %}
72
-            <div class="table-header">
73
-                <h2><i class="icon-sitemap icon-large"></i>{{ queryset_description }}</h2>
74
-            </div>
75 73
             <form action="." method="post">
76 74
                 {% csrf_token %}
77
-                <table class="table table-striped table-bordered">
78
-                    {% block product_list_header %}
79
-                        <tr>
80
-                            <th>
81
-                                {% if 'recently_edited' in request.GET %}
82
-                                    {% trans "Title" context "Product title" %}
83
-                                {% else %}
84
-                                    {% anchor 'title' _("Title") %}
85
-                                {% endif %}
86
-                            </th>
87
-                            <th>{% trans "UPC" %}</th>
88
-                            <th>{% trans "Image" %}</th>
89
-                            <th>{% trans "Product Type" %}</th>
90
-                            <th>{% trans "Variants" %}</th>
91
-                            <th>{% trans "Stock records" %}</th>
92
-                            <th></th>
93
-                        </tr>
94
-                    {% endblock %}
95
-                    {% for product in products %}
96
-                        {% block product %}
97
-                            <tr>
98
-                                <td><a href="{% url 'dashboard:catalogue-product' pk=product.id %}">{{ product.get_title }}</a></td>
99
-                                <td>{{ product.upc|default:"-" }}</td>
100
-                                <td>
101
-                                    {% if product.primary_image.original.url %}
102
-                                        {% with image=product.primary_image %}
103
-                                            {% thumbnail image.original "70x70" upscale=False as thumb %}
104
-                                            <a href="{{ image.original.url }}" rel="lightbox_{{ product.upc|default:"-" }}" class="sub-image">
105
-                                                <img src="{{ thumb.url }}" alt="{{ product.get_title }}" data-description="{% if image.caption %}{{ image.caption }}{% endif %}">
106
-                                            </a>
107
-                                            {% endthumbnail %}
108
-                                        {% endwith %}
109
-                                    {% else %}
110
-                                        -
111
-                                    {% endif %}
112
-                                </td>
113
-                                <td>{{ product.get_product_class.name }}</td>
114
-                                <td>
115
-                                    {% if product.is_standalone %}
116
-                                        -
117
-                                    {% else %}
118
-                                        {{ product.children.count }}
119
-                                    {% endif %}
120
-                                </td>
121
-                                <td>
122
-                                    {% if product.is_parent %}
123
-                                        -
124
-                                    {% else %}
125
-                                        {{ product.stockrecords.count }}
126
-                                    {% endif %}
127
-                                </td>
128
-                                <td>
129
-                                    <div class="btn-toolbar">
130
-                                        <div class="btn-group">
131
-                                            <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
132
-                                                {% trans "Actions" %}
133
-                                                <span class="caret"></span>
134
-                                            </a>
135
-                                            <ul class="dropdown-menu pull-right">
136
-                                                <li>
137
-                                                    <a href="{% url 'dashboard:catalogue-product' pk=product.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}">
138
-                                                        {% trans "Edit" %}
139
-                                                    </a>
140
-                                                </li>
141
-                                                {% if product.can_be_parent %}
142
-                                                    <li>
143
-                                                        <a href="{% url 'dashboard:catalogue-product-create-child' product.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}">
144
-                                                            {% trans "Add variant" %}
145
-                                                        </a>
146
-                                                    </li>
147
-                                                {% endif %}
148
-                                                <li>
149
-                                                    <a href="{{ product.get_absolute_url }}">
150
-                                                        {% trans "View on site" %}
151
-                                                    </a>
152
-                                                </li>
153
-                                                <li>
154
-                                                    <a href="{% url 'dashboard:catalogue-product-delete' pk=product.id %}">
155
-                                                        {% trans "Delete" %}
156
-                                                    </a>
157
-                                                </li>
158
-                                            </ul>
159
-                                        </div>
160
-                                    </div>
161
-                                </td>
162
-                            </tr>
163
-                        {% endblock product %}
164
-                    {% endfor %}
165
-                </table>
166
-                {% include "partials/pagination.html" %}
75
+                {% render_table products %}
167 76
             </form>
168 77
         {% endblock product_list %}
169 78
     {% else %}

+ 21
- 0
oscar/templates/oscar/dashboard/catalogue/product_row_actions.html Visa fil

@@ -0,0 +1,21 @@
1
+{% load django_tables2 %}
2
+{% load i18n %}
3
+<div class="btn-toolbar">
4
+    <div class="btn-group">
5
+        <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
6
+            {% trans "Actions" %}
7
+            <span class="caret"></span>
8
+        </a>
9
+        <ul class="dropdown-menu pull-right">
10
+            <li>
11
+                <a href="{% url 'dashboard:catalogue-product' pk=record.id %}{% querystring %}">{% trans "Edit" %}</a>
12
+            </li>
13
+            <li>
14
+                <a href="{{ record.get_absolute_url }}"> {% trans "View on site" %}</a>
15
+            </li>
16
+            <li>
17
+                <a href="{% url 'dashboard:catalogue-product-delete' pk=record.id %}">{% trans "Delete" %}</a>
18
+            </li>
19
+        </ul>
20
+    </div>
21
+</div>

+ 0
- 0
oscar/templates/oscar/dashboard/catalogue/product_row_image.html Visa fil


Vissa filer visades inte eftersom för många filer har ändrats

Laddar…
Avbryt
Spara