Browse Source

Merge branch 'master' into issue/1093/search

Conflicts:
	oscar/apps/dashboard/catalogue/views.py
	oscar/apps/search/facets.py
	tests/functional/catalogue/catalogue_tests.py
master
Maik Hoepfel 10 years ago
parent
commit
c46ff56049
100 changed files with 2644 additions and 1587 deletions
  1. 7
    5
      .travis.yml
  2. 9
    1
      Makefile
  3. 3
    13
      README.rst
  4. 35
    0
      create_migration.sh
  5. 1
    1
      docs/source/howto/how_to_configure_shipping.rst
  6. 28
    10
      docs/source/internals/getting_started.rst
  7. 0
    27
      docs/source/ref/settings.rst
  8. 160
    10
      docs/source/releases/v0.8.rst
  9. 4
    0
      docs/source/topics/class_loading_explained.rst
  10. 20
    0
      docs/source/topics/fork_app.rst
  11. 1
    1
      docs/source/topics/prices_and_availability.rst
  12. 1
    0
      oscar/__init__.py
  13. 12
    22
      oscar/app.py
  14. 1
    0
      oscar/apps/address/__init__.py
  15. 22
    4
      oscar/apps/address/abstract_models.py
  16. 8
    0
      oscar/apps/address/config.py
  17. 66
    126
      oscar/apps/address/migrations/0001_initial.py
  18. 7
    4
      oscar/apps/address/models.py
  19. 131
    0
      oscar/apps/address/south_migrations/0001_initial.py
  20. 0
    0
      oscar/apps/address/south_migrations/0002_auto__chg_field_useraddress_postcode.py
  21. 0
    0
      oscar/apps/address/south_migrations/0003_auto__add_field_country_display_order.py
  22. 0
    0
      oscar/apps/address/south_migrations/0004_convert_is_highlighted.py
  23. 0
    0
      oscar/apps/address/south_migrations/0005_auto__del_field_country_is_highlighted.py
  24. 0
    0
      oscar/apps/address/south_migrations/0006_auto__add_unique_useraddress_hash_user.py
  25. 0
    0
      oscar/apps/address/south_migrations/0007_auto__chg_field_useraddress_postcode.py
  26. 0
    0
      oscar/apps/address/south_migrations/0008_auto__chg_field_useraddress_phone_number.py
  27. 0
    0
      oscar/apps/address/south_migrations/0009_no_null_in_charfields.py
  28. 0
    0
      oscar/apps/address/south_migrations/0010_auto__chg_field_useraddress_first_name__chg_field_useraddress_title__c.py
  29. 0
    0
      oscar/apps/address/south_migrations/0011_auto__chg_field_useraddress_search_text.py
  30. 0
    0
      oscar/apps/address/south_migrations/0012_auto__del_index_country_iso_3166_1_a3__chg_field_country_iso_3166_1_nu.py
  31. 0
    0
      oscar/apps/address/south_migrations/__init__.py
  32. 1
    0
      oscar/apps/analytics/__init__.py
  33. 11
    3
      oscar/apps/analytics/abstract_models.py
  34. 11
    0
      oscar/apps/analytics/config.py
  35. 30
    254
      oscar/apps/analytics/migrations/0001_initial.py
  36. 74
    0
      oscar/apps/analytics/migrations/0002_auto_20140805_1510.py
  37. 17
    9
      oscar/apps/analytics/models.py
  38. 254
    0
      oscar/apps/analytics/south_migrations/0001_initial.py
  39. 0
    0
      oscar/apps/analytics/south_migrations/__init__.py
  40. 1
    0
      oscar/apps/basket/__init__.py
  41. 8
    2
      oscar/apps/basket/abstract_models.py
  42. 8
    0
      oscar/apps/basket/config.py
  43. 5
    5
      oscar/apps/basket/forms.py
  44. 16
    7
      oscar/apps/basket/middleware.py
  45. 32
    302
      oscar/apps/basket/migrations/0001_initial.py
  46. 63
    0
      oscar/apps/basket/migrations/0002_auto_20140805_1510.py
  47. 10
    6
      oscar/apps/basket/models.py
  48. 302
    0
      oscar/apps/basket/south_migrations/0001_initial.py
  49. 0
    0
      oscar/apps/basket/south_migrations/0002_auto__add_field_line_price_incl_tax.py
  50. 0
    0
      oscar/apps/basket/south_migrations/0003_auto__add_field_line_price_excl_tax.py
  51. 0
    0
      oscar/apps/basket/south_migrations/0004_auto__add_field_line_stockrecord.py
  52. 0
    0
      oscar/apps/basket/south_migrations/0005_auto__add_field_line_price_currency.py
  53. 0
    0
      oscar/apps/basket/south_migrations/0006_auto__chg_field_line_stockrecord.py
  54. 0
    0
      oscar/apps/basket/south_migrations/__init__.py
  55. 34
    38
      oscar/apps/basket/views.py
  56. 1
    0
      oscar/apps/catalogue/__init__.py
  57. 172
    54
      oscar/apps/catalogue/abstract_models.py
  58. 11
    0
      oscar/apps/catalogue/config.py
  59. 0
    4
      oscar/apps/catalogue/managers.py
  60. 291
    403
      oscar/apps/catalogue/migrations/0001_initial.py
  61. 38
    23
      oscar/apps/catalogue/models.py
  62. 9
    4
      oscar/apps/catalogue/reviews/abstract_models.py
  63. 5
    2
      oscar/apps/catalogue/reviews/app.py
  64. 62
    231
      oscar/apps/catalogue/reviews/migrations/0001_initial.py
  65. 7
    4
      oscar/apps/catalogue/reviews/models.py
  66. 237
    0
      oscar/apps/catalogue/reviews/south_migrations/0001_initial.py
  67. 0
    0
      oscar/apps/catalogue/reviews/south_migrations/0002_no_null_in_charfields.py
  68. 0
    0
      oscar/apps/catalogue/reviews/south_migrations/0003_auto__chg_field_productreview_name__chg_field_productreview_homepage__.py
  69. 0
    0
      oscar/apps/catalogue/reviews/south_migrations/__init__.py
  70. 4
    5
      oscar/apps/catalogue/reviews/views.py
  71. 403
    0
      oscar/apps/catalogue/south_migrations/0001_initial.py
  72. 0
    0
      oscar/apps/catalogue/south_migrations/0002_auto__add_field_product_status__add_field_category_description__add_fi.py
  73. 0
    0
      oscar/apps/catalogue/south_migrations/0003_auto__add_unique_product_upc__chg_field_productcontributor_role.py
  74. 0
    0
      oscar/apps/catalogue/south_migrations/0004_auto__chg_field_productattributevalue_value_boolean.py
  75. 0
    0
      oscar/apps/catalogue/south_migrations/0005_auto__chg_field_productattributevalue_value_boolean__add_field_product.py
  76. 0
    0
      oscar/apps/catalogue/south_migrations/0006_auto__add_field_product_is_discountable.py
  77. 0
    0
      oscar/apps/catalogue/south_migrations/0007_auto__add_field_productclass_requires_shipping__add_field_productclass.py
  78. 0
    0
      oscar/apps/catalogue/south_migrations/0008_auto__add_unique_option_code.py
  79. 0
    0
      oscar/apps/catalogue/south_migrations/0009_auto__add_field_product_rating.py
  80. 0
    0
      oscar/apps/catalogue/south_migrations/0010_call_update_product_ratings.py
  81. 0
    0
      oscar/apps/catalogue/south_migrations/0011_auto__chg_field_productimage_original__chg_field_category_image.py
  82. 0
    0
      oscar/apps/catalogue/south_migrations/0012_auto__chg_field_productattributevalue_value_boolean.py
  83. 0
    0
      oscar/apps/catalogue/south_migrations/0013_add_file_attributes.py
  84. 0
    0
      oscar/apps/catalogue/south_migrations/0014_auto__del_field_productcategory_is_canonical.py
  85. 0
    0
      oscar/apps/catalogue/south_migrations/0015_auto__chg_field_product_upc.py
  86. 0
    0
      oscar/apps/catalogue/south_migrations/0016_customer.py
  87. 0
    0
      oscar/apps/catalogue/south_migrations/0017_auto__del_contributor__del_productcontributor__del_contributorrole__de.py
  88. 0
    0
      oscar/apps/catalogue/south_migrations/0018_auto__chg_field_product_product_class.py
  89. 0
    0
      oscar/apps/catalogue/south_migrations/0019_no_null_in_charfields.py
  90. 0
    0
      oscar/apps/catalogue/south_migrations/0020_auto__chg_field_productattributevalue_value_text__chg_field_productatt.py
  91. 0
    0
      oscar/apps/catalogue/south_migrations/0021_auto__add_unique_productattributevalue_attribute_product__add_unique_p.py
  92. 0
    0
      oscar/apps/catalogue/south_migrations/0022_auto__del_field_product_score.py
  93. 0
    0
      oscar/apps/catalogue/south_migrations/0023_auto.py
  94. 0
    0
      oscar/apps/catalogue/south_migrations/0024_auto__del_attributeentity__del_attributeentitytype__del_field_producta.py
  95. 0
    0
      oscar/apps/catalogue/south_migrations/0025_auto__add_field_product_structure.py
  96. 0
    0
      oscar/apps/catalogue/south_migrations/0026_determine_product_structure.py
  97. 0
    0
      oscar/apps/catalogue/south_migrations/__init__.py
  98. 10
    7
      oscar/apps/catalogue/views.py
  99. 1
    0
      oscar/apps/checkout/__init__.py
  100. 0
    0
      oscar/apps/checkout/config.py

+ 7
- 5
.travis.yml View File

@@ -10,16 +10,18 @@ env:
10 10
     # $TRANSIFEX_PASSWORD for oscar_bot (used in transifex.sh)
11 11
     secure: FuIlzEsGJiAwhaIRBmRNsq9eXmuzs25fX6BChknW4lDyVAySWMp0+Zps9Bd0JgfFYUG3Ip+OTmksYIoTUsG25ZJS9cq1IFt3QKUAN70YCI/4ZBLeIdICPEyxq+Km179+NeEXmBUug17RLMLxh3MWfO+RKUHK9yHIPNNpq0dNyoo=
12 12
   matrix:
13
-    - DJANGO_VERSION=1.5.8
14
-    - DJANGO_VERSION=1.6.5
13
+    - DJANGO=Django==1.6.5
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
-  - easy_install Django==$DJANGO_VERSION
24
+  - easy_install $DJANGO
23 25
 
24 26
 before_script:
25 27
     # Create testing databases for running migrations against

+ 9
- 1
Makefile View File

@@ -12,10 +12,11 @@ sandbox: install
12 12
 	-rm -rf sites/sandbox/public/static
13 13
 	-rm -f sites/sandbox/db.sqlite
14 14
 	# Create database
15
+	# 'syncdb' is identical to migrate in Django 1.7+; but calling it twice should have no effect
15 16
 	sites/sandbox/manage.py syncdb --noinput
16 17
 	sites/sandbox/manage.py migrate
17 18
 	# Import some fixtures. Order is important as JSON fixtures include primary keys
18
-	sites/sandbox/manage.py loaddata sites/sandbox/fixtures/variants.json
19
+	sites/sandbox/manage.py loaddata sites/sandbox/fixtures/child_products.json
19 20
 	sites/sandbox/manage.py oscar_import_catalogue sites/sandbox/fixtures/*.csv
20 21
 	sites/sandbox/manage.py oscar_import_catalogue_images sites/sandbox/fixtures/images.tar.gz
21 22
 	sites/sandbox/manage.py oscar_populate_countries
@@ -121,3 +122,10 @@ clean:
121 122
 preflight: lint
122 123
     # Bare minimum of tests to run before pushing to master
123 124
 	./runtests.py
125
+
126
+todo:
127
+	# Look for areas of the code that need updating when some event has taken place (like 
128
+	# Oscar dropping support for a Django version)
129
+	-grep -rnH TODO *.txt
130
+	-grep -rnH TODO oscar/apps/
131
+	-grep -rnH "django.VERSION" oscar/apps

+ 3
- 13
README.rst View File

@@ -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

+ 35
- 0
create_migration.sh View File

@@ -0,0 +1,35 @@
1
+#!/usr/bin/env bash
2
+#
3
+# Rather verbose and destructive script to create both South migrations and the
4
+# new native migrations (which we need as we support both Django 1.6 and 1.7).
5
+# This will install and uninstall Django versions in your virtualenv, only work
6
+# with the default SQLite database, destroy that database repeatedly
7
+
8
+# Grab current version of Django from virtualenv
9
+DJANGO_VERSION=$(pip freeze | awk 'BEGIN {FS="=="} /Django/ {print $2}')
10
+SOUTH_VERSION=$(pip freeze | awk 'BEGIN {FS="=="} /South/ {print $2}')
11
+
12
+APPS=( analytics checkout address shipping catalogue reviews partner basket payment \
13
+       offer order customer promotions search voucher wishlists )
14
+
15
+echo "Uninstalling Django(==$DJANGO_VERSION) and South(==$SOUTH_VERSION)"
16
+pip uninstall Django South -y
17
+
18
+echo "Generating Django-native (>=1.7) migrations"
19
+pip install https://www.djangoproject.com/download/1.7c2/tarball/
20
+rm -f sites/sandbox/db.sqlite
21
+sites/sandbox/manage.py migrate
22
+sites/sandbox/manage.py makemigrations ${APPS[@]}
23
+
24
+echo "Generating Django 1.6 migrations"
25
+pip install "Django==1.6.5" "South==1.0"
26
+rm -f sites/sandbox/db.sqlite
27
+sites/sandbox/manage.py syncdb --noinput
28
+sites/sandbox/manage.py migrate
29
+for APP in "${APPS[@]}"
30
+do
31
+    sites/sandbox/manage.py schemamigration $APP --auto
32
+done
33
+
34
+echo "Restoring Django(==$DJANGO_VERSION) and South(==$SOUTH_VERSION)"
35
+pip install "Django==$DJANGO_VERSION" "South==$SOUTH_VERSION"

+ 1
- 1
docs/source/howto/how_to_configure_shipping.rst View File

@@ -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

+ 28
- 10
docs/source/internals/getting_started.rst View File

@@ -84,7 +84,6 @@ and append Oscar's core apps:
84 84
         'django.contrib.staticfiles',
85 85
         'django.contrib.flatpages',
86 86
         ...
87
-        'south',
88 87
         'compressor',
89 88
     ] + get_core_apps()
90 89
 
@@ -107,21 +106,16 @@ More info about installing ``flatpages`` is in the `Django docs`_.
107 106
 
108 107
 Next, add ``oscar.apps.basket.middleware.BasketMiddleware`` and
109 108
 ``django.contrib.flatpages.middleware.FlatpageFallbackMiddleware`` to
110
-your ``MIDDLEWARE_CLASSES`` setting. If you're running on Django 1.5, it is
111
-also recommended to use ``django.middleware.transaction.TransactionMiddleware``:
109
+your ``MIDDLEWARE_CLASSES`` setting.
112 110
 
113 111
 .. code-block:: django
114 112
 
115 113
     MIDDLEWARE_CLASSES = (
116 114
         ...
117 115
         'oscar.apps.basket.middleware.BasketMiddleware',
118
-        'django.middleware.transaction.TransactionMiddleware',  # Django 1.5 only
119 116
         'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
120 117
     )
121 118
 
122
-If you're running Django 1.6 or above, you should enable ``ATOMIC_REQUESTS``
123
-instead (see database settings above).
124
-
125 119
 Set your auth backends to:
126 120
 
127 121
 .. code-block:: django
@@ -237,7 +231,7 @@ Check your database settings. A quick way to get started is to use SQLite:
237 231
             'PASSWORD': '',
238 232
             'HOST': '',
239 233
             'PORT': '',
240
-            'ATOMIC_REQUESTS': True,  # Django 1.6+
234
+            'ATOMIC_REQUESTS': True,
241 235
         }
242 236
     }
243 237
 
@@ -255,8 +249,32 @@ Then create the database and the shop should be browsable:
255 249
 You should now have an empty, but running Oscar install that you can browse at
256 250
 http://localhost:8000.
257 251
 
258
-Fixtures
259
-========
252
+Migrations
253
+----------
254
+
255
+Oscar ships with two sets of migrations. If you're running Django 1.7, you
256
+don't need to do anything; Django's migration framework will detect them
257
+automatically and will do the right thing.
258
+If you're running Django 1.6, you need to install `South`_::
259
+
260
+.. code-block:: bash
261
+
262
+    $ pip install South
263
+
264
+And you need to add it to your installed apps:
265
+
266
+.. code-block:: django
267
+
268
+    INSTALLED_APPS = [
269
+        ...
270
+        'south',
271
+    ] + get_core_apps()
272
+
273
+.. _South: http://south.readthedocs.org/en/latest/
274
+
275
+
276
+Initial data
277
+============
260 278
 
261 279
 The default checkout process requires a shipping address with a country.  Oscar
262 280
 uses a model for countries with flags that indicate which are valid shipping

+ 0
- 27
docs/source/ref/settings.rst View File

@@ -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
 
@@ -364,13 +344,6 @@ Default: ``'oscar_open_basket'``
364 344
 
365 345
 The name of the cookie for the open basket.
366 346
 
367
-``OSCAR_BASKET_COOKIE_SAVED``
368
------------------------------
369
-
370
-Default: ``'oscar_saved_basket'``
371
-
372
-The name of the cookie for the saved basket.
373
-
374 347
 Currency settings
375 348
 =================
376 349
 

+ 160
- 10
docs/source/releases/v0.8.rst View File

@@ -37,10 +37,12 @@ availability logic now needs to be handled with strategies.
37 37
 Compatibility
38 38
 -------------
39 39
 
40
-Oscar 0.8 is compatible with Django 1.5-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.
41 42
 
42
-Support for Python 2.6 has been dropped; Oscar works with Python 2.7, 3.3
43
-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.
44 46
 
45 47
 .. _new_in_0.8:
46 48
 
@@ -80,6 +82,27 @@ has been altered to be:
80 82
 Some properties and method names have also been updated to the new naming. The
81 83
 old ones will throw a deprecation warning.
82 84
 
85
+Better handling of child products in product dashboard
86
+------------------------------------------------------
87
+Together with the changes above, the dashboard experience for child products
88
+has been improved. The difference between a parent product and a stand-alone
89
+product is hidden from the user; a user can now add and remove child products
90
+on any suitable product. When the first child product is added, a stand-alone
91
+product becomes a parent product; and vice versa.
92
+In the front-end, the old name of "product variants" has been kept.
93
+
94
+Django 1.7 support
95
+------------------
96
+
97
+Oscar 0.8 comes with support for Django 1.7 out of the box. The app refactor
98
+and the new migration framework are both great improvements to Django. Oscar
99
+now ships with sets of migrations both for South and the new native
100
+migrations framework.
101
+Unfortunately, the changes in Django required a few breaking changes when
102
+upgrading Oscar both for users staying on Django 1.6 and for users upgrading to
103
+Django 1.7 at the same time. These are detailed in the section for
104
+backwards-incompatible changes.
105
+
83 106
 Reworked shipping app
84 107
 ~~~~~~~~~~~~~~~~~~~~~
85 108
 
@@ -204,17 +227,60 @@ Minor changes
204 227
   populate the country databases. It replaces the ``countries.json`` fixture.
205 228
   The command relies on the ``pycountry`` library being installed.
206 229
 
230
+* It is now possible to use product attributes to add a relation to arbitrary
231
+  model instances. There was some (presumably broken) support for it before,
232
+  but you should now be able to use product attributes of type ``entity`` as
233
+  expected. There's currently no frontend or dashboard support for it, as there
234
+  is no good default behaviour.
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
+* ``jquery-ui-datepicker`` has been replaced in the dashboard by
241
+  bootstrap-datetimepicker_. We still ship with ``jquery-ui-datepicker`` and
242
+  ``JQuery UI`` as it's in use in the frontend.
243
+
244
+.. _django-tables2: http://django-tables2.readthedocs.org/en/latest/
245
+.. _bootstrap-datetimepicker: http://www.malot.fr/bootstrap-datetimepicker/
246
+
207 247
 .. _incompatible_changes_in_0.8:
208 248
 
209 249
 Backwards incompatible changes in 0.8
210 250
 -------------------------------------
211 251
 
212
-.. _incompatible_shipping_changes_in_0.8:
252
+Migrations
253
+~~~~~~~~~~
254
+
255
+* South is not a dependency of Oscar anymore: This means it won't get installed
256
+  automatically when you install Oscar. If you are on Django 1.6 and want to
257
+  use South, you will need to explicitly install it and add it to your
258
+  requirements.
259
+* Only South >= 1.0 is supported: South 1.0 is a backwards compatible release
260
+  explicitly released to help with the upgrade path to Django 1.7. Please make
261
+  sure you update accordingly if you intend to keep using South. Older versions
262
+  of South will look in the wrong directories and will break with this Oscar
263
+  release.
264
+* Rename your South ``migrations`` directories: To avoid
265
+  clashes between Django's migrations and South's migrations, you should rename
266
+  all your South migrations directories (including those of forked Oscar apps)
267
+  to ``south_migrations``. South 1.0 will check those first before falling back
268
+  to ``migrations``.
269
+* Upgrading to new-style migrations: If you're upgrading to Django 1.7, you
270
+  will need to follow the `instructions to upgrade from South`_ for your own
271
+  apps. For any forked Oscar apps, you will need to copy Oscar's initial
272
+  migrations into your emptied ``migrations`` directory first, because Oscar's
273
+  set of migrations depend on each other. You can then create migrations for
274
+  your changes by calling ``./manage.py makemigrations``. Django should
275
+  detect that the database layout already matches the state of migrations; so
276
+  a call to ``migrate`` should fake the migrations.
277
+
278
+.. _instructions to upgrade from South: https://docs.djangoproject.com/en/1.7/topics/migrations/#upgrading-from-south
213 279
 
214 280
 Product structure
215 281
 ~~~~~~~~~~~~~~~~~
216 282
 
217
-Generally, backwards compatibility has been preserved. Two changes are
283
+Generally, backwards compatibility has been preserved. Those changes are
218 284
 unavoidable:
219 285
 
220 286
 * You now need to explicitly set product structure when creating a product;
@@ -224,6 +290,7 @@ unavoidable:
224 290
   deprecation warning), but if you used the old related name in a query lookup
225 291
   (e.g. ``products.filter(variants__title='foo')``, you will have to change it
226 292
   to ``children``.
293
+* Template blocks and CSS classes have been renamed.
227 294
 
228 295
 The following methods and properties have been deprecated:
229 296
 
@@ -236,6 +303,22 @@ The following methods and properties have been deprecated:
236 303
 * ``Strategy.select_variant_stockrecords`` - Use
237 304
   ``select_children_stockrecords`` instead.
238 305
 
306
+Furthermore, CSS classes and template blocks have been updated. Please follow
307
+the following renaming pattern:
308
+* ``variant-product`` becomes ``child-product``
309
+* ``product_variants`` becomes ``child_products``
310
+* ``variants`` becomes ``children``
311
+* ``variant`` becomes ``child``
312
+
313
+Product editing
314
+---------------
315
+The dashboard improvements for child products meant slight changes to both
316
+``ProductCreateUpdateView`` and ``ProductForm``. Notably, ``ProductForm`` now
317
+gets a ``parent`` kwarg. Please review your customisations for compatibility
318
+with the updated code.
319
+
320
+.. _incompatible_shipping_changes_in_0.8:
321
+
239 322
 Shipping
240 323
 ~~~~~~~~
241 324
 
@@ -341,6 +424,41 @@ Other potentially breaking changes related to shipping include:
341 424
 * ``oscar.apps.shipping.Scales`` has been renamed and moved to
342 425
   ``oscar.apps.shipping.scales.Scale``, and is now overridable.
343 426
 
427
+Email address handling
428
+----------------------
429
+
430
+In theory, the local part of an email is case-sensitive. In practice, many
431
+users don't know about this and most email servers don't consider the
432
+capitalisation. Because of this, Oscar now disregards capitalisation when
433
+looking up emails (e.g. when a user logs in).
434
+Storing behaviour is unaltered: When a user's email address is stored (e.g.
435
+when registering or checking out), the local part is unaltered and
436
+the host portion is lowercased.
437
+
438
+.. warning::
439
+
440
+   Those changes mean you might now have multiple users with email addresses
441
+   that Oscar considers identical. Please use the new
442
+   ``oscar_find_duplicate_emails`` management command to check your database
443
+   and deal with any conflicts accordingly.
444
+
445
+Django 1.7 support
446
+------------------
447
+
448
+If you have any plans to upgrade to Django 1.7, more changes beyond
449
+addressing migrations are necessary:
450
+
451
+* You should be aware that Django 1.7 now enforces uniqueness of app labels.
452
+  Oscar dashboard apps now ship with app configs that set their app label
453
+  to ``{oldname}_dashboard``.
454
+* If you have forked any Oscar apps, you must add app configs to them, and
455
+  have them inherit from the Oscar one. See the appropriate section in
456
+  :doc:`/topics/fork_app` for an example.
457
+* Double-check that you address migrations as detailed above.
458
+* Django now enforces that no calls happen to the model registry during
459
+  app startup. This mostly means that you should avoid module-level calls to
460
+  ``get_model``, as that only works with a fully initialised model registry.
461
+
344 462
 Misc
345 463
 ~~~~
346 464
 
@@ -377,11 +495,27 @@ Misc
377 495
   trivial ``ProductAttribute.get_validator`` and the unused
378 496
   ``ProductAttribute.is_value_valid`` methods have been removed.
379 497
 
380
-* It is now possible to use product attributes to add a relation to arbitrary
381
-  model instances. There was some (presumably broken) support for it before,
382
-  but you should now be able to use product attributes of type ``entity`` as
383
-  expected. There's currently no frontend or dashboard support for it, as there
384
-  is no good default behaviour.
498
+* The ``RangeProductFileUpload`` model has been moved from the ranges
499
+  dashboard app to the offers app. The migrations that have been naively
500
+  drop and re-create the model; any data is lost! This is probably not an
501
+  issue, as the model is only used while an range upload is in progress. If
502
+  you need to keep the data, ensure you migrate it across.
503
+
504
+* ``oscar.core.loading.get_model`` now raises a ``LookupError`` instead of an
505
+  ``ImportError`` if a model can't be found. That brings it more in line with
506
+  what Django does since the app refactor.
507
+
508
+* ``CommunicationEventType.category`` was storing a localised string, which
509
+  breaks when switching locale. It now uses ``choices`` to map between the
510
+  value and a localised string. Unfortunately, if you're using this feature
511
+  and not running an English locale, you will need to migrate the existing
512
+  data to the English values.
513
+
514
+* Support for the ``OSCAR_OFFER_BLACKLIST_PRODUCT`` setting has been removed.
515
+  It was only partially supported: it prevented products from being
516
+  added to a range, but offers could be applied to the products nonetheless.
517
+  To prevent an offer being applied to a product, use ``is_discountable`` or
518
+  override ``get_is_discountable`` on your product instances.
385 519
 
386 520
 * ``Category.get_ancestors`` used to return a list of ancestors and would
387 521
   default to include itself. For consistency with get_descendants and to avoid
@@ -455,6 +589,12 @@ Migrations
455 589
     Please double-check it's outcome and make sure to do something similar
456 590
     if you have forked the catalogue app.
457 591
 
592
+.. note::
593
+
594
+    The migration numbers below refer to the numbers of the South migrations.
595
+    Oscar 0.8 ships with a set of new initial migrations for Django's new
596
+    native migrations framework. They include all the changes detailed below.
597
+
458 598
 .. note::
459 599
 
460 600
     Be sure to read the detailed instructions for
@@ -475,6 +615,11 @@ Migrations
475 615
       and do entity attribute changes and model deletions.
476 616
     - ``0025`` & ``0026`` - Schema & data migration to determine and save Product structure.
477 617
 
618
+* Offer:
619
+
620
+    - ``0033`` - Use an ``AutoSlug`` field for ``Range`` models
621
+    - ``0034`` - Add moved ``RangedProductFileUpload`` model.
622
+
478 623
 * Order:
479 624
 
480 625
     - ``0029`` - Add ``unique_together`` to ``PaymentEventQuantity`` and ``ShippingEventQuantity``
@@ -489,6 +634,11 @@ Migrations
489 634
 
490 635
     - ``0006`` - Add ``unique_together`` to ``OrderedProduct``
491 636
 
637
+* Ranges dashboard:
638
+
639
+    - ``0003`` - Drop ``RangeProductFileUpload`` from ``ranges`` app. This is
640
+                 a destructive change!
641
+
492 642
 * Shipping:
493 643
 
494 644
     - ``0007`` - Change ``WeightBand.upper_limit`` from ``FloatField`` to ``DecimalField``

+ 4
- 0
docs/source/topics/class_loading_explained.rst View File

@@ -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
 -------

+ 20
- 0
docs/source/topics/fork_app.rst View File

@@ -52,3 +52,23 @@ the core app's ``admin.py`` (which will run the register code)::
52 52
     import oscar.apps.order.admin
53 53
 
54 54
 This isn't great but we haven't found a better way as of yet.
55
+
56
+Django 1.7+: Use supplied app config
57
+====================================
58
+
59
+Oscar ships with an app config for each app, which sets app labels and
60
+runs startup code. You need to make sure that happens.
61
+
62
+.. code-block: django
63
+
64
+    # yourproject/order/config.py
65
+
66
+    from oscar.apps.order import config
67
+
68
+
69
+    class OrderConfig(config.OrderConfig):
70
+        name = 'yourproject.order'
71
+
72
+    # yourproject/order/__init__.py
73
+
74
+    default_app_config = 'yourproject.order.config.OrderConfig'

+ 1
- 1
docs/source/topics/prices_and_availability.rst View File

@@ -276,7 +276,7 @@ Here's an example ``strategy.py`` module which is used to charge VAT on prices.
276 276
         """
277 277
 
278 278
         def strategy(self, request=None, user=None, **kwargs):
279
-            return UKStrategy(territory)
279
+            return UKStrategy()
280 280
 
281 281
 
282 282
     class IncludingVAT(strategy.FixedRateTax):

+ 1
- 0
oscar/__init__.py View File

@@ -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
 

+ 12
- 22
oscar/app.py View File

@@ -48,37 +48,27 @@ class Shop(Application):
48 48
                 login_forbidden(auth_views.password_reset_done),
49 49
                 name='password-reset-done')]
50 50
 
51
-        # Django <=1.5: uses uidb36 to encode the user's primary key
51
+        # Django <=1.5: uses uidb36 to encode the user's primary key (support has been removed)
52 52
         # Django 1.6:   uses uidb64 to encode the user's primary key, but
53 53
         #               but supports legacy links
54 54
         # Django > 1.7: used uidb64 to encode the user's primary key
55 55
         # see https://docs.djangoproject.com/en/dev/releases/1.6/#django-contrib-auth-password-reset-uses-base-64-encoding-of-user-pk
56
-        if django.VERSION < (1, 6):
56
+        urls.append(
57
+            url(r'^password-reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>.+)/$',
58
+                login_forbidden(auth_views.password_reset_confirm),
59
+                {
60
+                    'post_reset_redirect': reverse_lazy('password-reset-complete'),
61
+                    'set_password_form': self.set_password_form,
62
+                },
63
+                name='password-reset-confirm'))
64
+        if django.VERSION < (1, 7):
57 65
             urls.append(
58 66
                 url(r'^password-reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',
59
-                    login_forbidden(auth_views.password_reset_confirm),
67
+                    login_forbidden(auth_views.password_reset_confirm_uidb36),
60 68
                     {
61 69
                         'post_reset_redirect': reverse_lazy('password-reset-complete'),
62 70
                         'set_password_form': self.set_password_form,
63
-                    },
64
-                    name='password-reset-confirm'))
65
-        else:
66
-            urls.append(
67
-                url(r'^password-reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>.+)/$',
68
-                    login_forbidden(auth_views.password_reset_confirm),
69
-                    {
70
-                        'post_reset_redirect': reverse_lazy('password-reset-complete'),
71
-                        'set_password_form': self.set_password_form,
72
-                    },
73
-                    name='password-reset-confirm'))
74
-            if django.VERSION < (1, 7):
75
-                urls.append(
76
-                    url(r'^password-reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',
77
-                        login_forbidden(auth_views.password_reset_confirm_uidb36),
78
-                        {
79
-                            'post_reset_redirect': reverse_lazy('password-reset-complete'),
80
-                            'set_password_form': self.set_password_form,
81
-                        }))
71
+                    }))
82 72
 
83 73
         urls += [
84 74
             url(r'^password-reset/complete/$',

+ 1
- 0
oscar/apps/address/__init__.py View File

@@ -0,0 +1 @@
1
+default_app_config = 'oscar.apps.address.config.AddressConfig'

+ 22
- 4
oscar/apps/address/abstract_models.py View File

@@ -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.
@@ -402,11 +405,12 @@ class AbstractCountry(models.Model):
402 405
 
403 406
     class Meta:
404 407
         abstract = True
408
+        app_label = 'address'
405 409
         verbose_name = _('Country')
406 410
         verbose_name_plural = _('Countries')
407 411
         ordering = ('-display_order', 'printable_name',)
408 412
 
409
-    def __unicode__(self):
413
+    def __str__(self):
410 414
         return self.printable_name or self.name
411 415
 
412 416
     @property
@@ -435,7 +439,16 @@ class AbstractShippingAddress(AbstractAddress):
435 439
 
436 440
     A shipping address should not be edited once the order has been placed -
437 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.
438 450
     """
451
+
439 452
     phone_number = PhoneNumberField(
440 453
         _("Phone number"), blank=True,
441 454
         help_text=_("In case we need to call you about your order"))
@@ -446,6 +459,8 @@ class AbstractShippingAddress(AbstractAddress):
446 459
 
447 460
     class Meta:
448 461
         abstract = True
462
+        # ShippingAddress is registered in order/models.py
463
+        app_label = 'order'
449 464
         verbose_name = _("Shipping address")
450 465
         verbose_name_plural = _("Shipping addresses")
451 466
 
@@ -518,6 +533,7 @@ class AbstractUserAddress(AbstractShippingAddress):
518 533
 
519 534
     class Meta:
520 535
         abstract = True
536
+        app_label = 'address'
521 537
         verbose_name = _("User address")
522 538
         verbose_name_plural = _("User addresses")
523 539
         ordering = ['-num_orders']
@@ -537,9 +553,10 @@ class AbstractUserAddress(AbstractShippingAddress):
537 553
 
538 554
 
539 555
 class AbstractBillingAddress(AbstractAddress):
540
-
541 556
     class Meta:
542 557
         abstract = True
558
+        # BillingAddress is registered in order/models.py
559
+        app_label = 'order'
543 560
         verbose_name = _("Billing address")
544 561
         verbose_name_plural = _("Billing addresses")
545 562
 
@@ -564,5 +581,6 @@ class AbstractPartnerAddress(AbstractAddress):
564 581
 
565 582
     class Meta:
566 583
         abstract = True
584
+        app_label = 'partner'
567 585
         verbose_name = _("Partner address")
568 586
         verbose_name_plural = _("Partner addresses")

+ 8
- 0
oscar/apps/address/config.py View File

@@ -0,0 +1,8 @@
1
+from django.apps import AppConfig
2
+from django.utils.translation import ugettext_lazy as _
3
+
4
+
5
+class AddressConfig(AppConfig):
6
+    label = 'address'
7
+    name = 'oscar.apps.address'
8
+    verbose_name = _('Address')

+ 66
- 126
oscar/apps/address/migrations/0001_initial.py View File

@@ -1,131 +1,71 @@
1
-# encoding: utf-8
2
-import datetime
3
-from south.db import db
4
-from south.v2 import SchemaMigration
5
-from django.db import models
6
-from oscar.core.compat import AUTH_USER_MODEL, AUTH_USER_MODEL_NAME
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
7 3
 
8
-class Migration(SchemaMigration):
4
+from django.db import models, migrations
5
+import oscar.models.fields
6
+from django.conf import settings
9 7
 
10
-    def forwards(self, orm):
11 8
 
12
-        # Adding model 'UserAddress'
13
-        db.create_table('address_useraddress', (
14
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
15
-            ('title', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)),
16
-            ('first_name', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
17
-            ('last_name', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
18
-            ('line1', self.gf('django.db.models.fields.CharField')(max_length=255)),
19
-            ('line2', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
20
-            ('line3', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
21
-            ('line4', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
22
-            ('state', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
23
-            ('postcode', self.gf('django.db.models.fields.CharField')(max_length=64)),
24
-            ('country', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['address.Country'])),
25
-            ('search_text', self.gf('django.db.models.fields.CharField')(max_length=1000)),
26
-            ('phone_number', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)),
27
-            ('notes', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
28
-            ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='addresses', to=orm[AUTH_USER_MODEL])),
29
-            ('is_default_for_shipping', self.gf('django.db.models.fields.BooleanField')(default=False)),
30
-            ('is_default_for_billing', self.gf('django.db.models.fields.BooleanField')(default=False)),
31
-            ('num_orders', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
32
-            ('hash', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
33
-            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
34
-        ))
35
-        db.send_create_signal('address', ['UserAddress'])
9
+class Migration(migrations.Migration):
36 10
 
37
-        # Adding model 'Country'
38
-        db.create_table('address_country', (
39
-            ('iso_3166_1_a2', self.gf('django.db.models.fields.CharField')(max_length=2, primary_key=True)),
40
-            ('iso_3166_1_a3', self.gf('django.db.models.fields.CharField')(max_length=3, null=True, db_index=True)),
41
-            ('iso_3166_1_numeric', self.gf('django.db.models.fields.PositiveSmallIntegerField')(null=True, db_index=True)),
42
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=128)),
43
-            ('printable_name', self.gf('django.db.models.fields.CharField')(max_length=128)),
44
-            ('is_highlighted', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True)),
45
-            ('is_shipping_country', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True)),
46
-        ))
47
-        db.send_create_signal('address', ['Country'])
11
+    dependencies = [
12
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13
+    ]
48 14
 
49
-
50
-    def backwards(self, orm):
51
-
52
-        # Deleting model 'UserAddress'
53
-        db.delete_table('address_useraddress')
54
-
55
-        # Deleting model 'Country'
56
-        db.delete_table('address_country')
57
-
58
-
59
-    models = {
60
-        'address.country': {
61
-            'Meta': {'ordering': "('-is_highlighted', 'name')", 'object_name': 'Country'},
62
-            'is_highlighted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
63
-            'is_shipping_country': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
64
-            'iso_3166_1_a2': ('django.db.models.fields.CharField', [], {'max_length': '2', 'primary_key': 'True'}),
65
-            'iso_3166_1_a3': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'db_index': 'True'}),
66
-            'iso_3166_1_numeric': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'db_index': 'True'}),
67
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
68
-            'printable_name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
69
-        },
70
-        'address.useraddress': {
71
-            'Meta': {'ordering': "['-num_orders']", 'object_name': 'UserAddress'},
72
-            'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['address.Country']"}),
73
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
74
-            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
75
-            'hash': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
76
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
77
-            'is_default_for_billing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
78
-            'is_default_for_shipping': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
79
-            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
80
-            'line1': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
81
-            'line2': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
82
-            'line3': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
83
-            'line4': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
84
-            'notes': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
85
-            'num_orders': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
86
-            'phone_number': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
87
-            'postcode': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
88
-            'search_text': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
89
-            'state': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
90
-            'title': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
91
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'addresses'", 'to': "orm['{0}']".format(AUTH_USER_MODEL)})
92
-        },
93
-        'auth.group': {
94
-            'Meta': {'object_name': 'Group'},
95
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
96
-            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
97
-            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
98
-        },
99
-        'auth.permission': {
100
-            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
101
-            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
102
-            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
103
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
104
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
105
-        },
106
-        AUTH_USER_MODEL: {
107
-            'Meta': {'object_name': AUTH_USER_MODEL_NAME},
108
-            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
109
-            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
110
-            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
111
-            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
112
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
113
-            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
114
-            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
115
-            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
116
-            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
117
-            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
118
-            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
119
-            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
120
-            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
121
-        },
122
-        'contenttypes.contenttype': {
123
-            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
124
-            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
125
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
126
-            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
127
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
128
-        }
129
-    }
130
-
131
-    complete_apps = ['address']
15
+    operations = [
16
+        migrations.CreateModel(
17
+            name='Country',
18
+            fields=[
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
+            ],
27
+            options={
28
+                'verbose_name': 'Country',
29
+                'verbose_name_plural': 'Countries',
30
+                'ordering': ('-display_order', 'printable_name'),
31
+                'abstract': False,
32
+            },
33
+            bases=(models.Model,),
34
+        ),
35
+        migrations.CreateModel(
36
+            name='UserAddress',
37
+            fields=[
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
+                ('country', models.ForeignKey(verbose_name='Country', to='address.Country')),
57
+                ('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL)),
58
+            ],
59
+            options={
60
+                'verbose_name': 'User address',
61
+                'verbose_name_plural': 'User addresses',
62
+                'ordering': ['-num_orders'],
63
+                'abstract': False,
64
+            },
65
+            bases=(models.Model,),
66
+        ),
67
+        migrations.AlterUniqueTogether(
68
+            name='useraddress',
69
+            unique_together=set([('user', 'hash')]),
70
+        ),
71
+    ]

+ 7
- 4
oscar/apps/address/models.py View File

@@ -1,10 +1,13 @@
1
+from oscar.core.loading import is_model_registered
1 2
 from oscar.apps.address.abstract_models import (
2 3
     AbstractUserAddress, AbstractCountry)
3 4
 
4 5
 
5
-class UserAddress(AbstractUserAddress):
6
-    pass
6
+if not is_model_registered('address', 'UserAddress'):
7
+    class UserAddress(AbstractUserAddress):
8
+        pass
7 9
 
8 10
 
9
-class Country(AbstractCountry):
10
-    pass
11
+if not is_model_registered('address', 'Country'):
12
+    class Country(AbstractCountry):
13
+        pass

+ 131
- 0
oscar/apps/address/south_migrations/0001_initial.py View File

@@ -0,0 +1,131 @@
1
+# encoding: utf-8
2
+import datetime
3
+from south.db import db
4
+from south.v2 import SchemaMigration
5
+from django.db import models
6
+from oscar.core.compat import AUTH_USER_MODEL, AUTH_USER_MODEL_NAME
7
+
8
+class Migration(SchemaMigration):
9
+
10
+    def forwards(self, orm):
11
+
12
+        # Adding model 'UserAddress'
13
+        db.create_table('address_useraddress', (
14
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
15
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)),
16
+            ('first_name', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
17
+            ('last_name', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
18
+            ('line1', self.gf('django.db.models.fields.CharField')(max_length=255)),
19
+            ('line2', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
20
+            ('line3', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
21
+            ('line4', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
22
+            ('state', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
23
+            ('postcode', self.gf('django.db.models.fields.CharField')(max_length=64)),
24
+            ('country', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['address.Country'])),
25
+            ('search_text', self.gf('django.db.models.fields.CharField')(max_length=1000)),
26
+            ('phone_number', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)),
27
+            ('notes', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
28
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='addresses', to=orm[AUTH_USER_MODEL])),
29
+            ('is_default_for_shipping', self.gf('django.db.models.fields.BooleanField')(default=False)),
30
+            ('is_default_for_billing', self.gf('django.db.models.fields.BooleanField')(default=False)),
31
+            ('num_orders', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
32
+            ('hash', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
33
+            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
34
+        ))
35
+        db.send_create_signal('address', ['UserAddress'])
36
+
37
+        # Adding model 'Country'
38
+        db.create_table('address_country', (
39
+            ('iso_3166_1_a2', self.gf('django.db.models.fields.CharField')(max_length=2, primary_key=True)),
40
+            ('iso_3166_1_a3', self.gf('django.db.models.fields.CharField')(max_length=3, null=True, db_index=True)),
41
+            ('iso_3166_1_numeric', self.gf('django.db.models.fields.PositiveSmallIntegerField')(null=True, db_index=True)),
42
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=128)),
43
+            ('printable_name', self.gf('django.db.models.fields.CharField')(max_length=128)),
44
+            ('is_highlighted', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True)),
45
+            ('is_shipping_country', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True)),
46
+        ))
47
+        db.send_create_signal('address', ['Country'])
48
+
49
+
50
+    def backwards(self, orm):
51
+
52
+        # Deleting model 'UserAddress'
53
+        db.delete_table('address_useraddress')
54
+
55
+        # Deleting model 'Country'
56
+        db.delete_table('address_country')
57
+
58
+
59
+    models = {
60
+        'address.country': {
61
+            'Meta': {'ordering': "('-is_highlighted', 'name')", 'object_name': 'Country'},
62
+            'is_highlighted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
63
+            'is_shipping_country': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
64
+            'iso_3166_1_a2': ('django.db.models.fields.CharField', [], {'max_length': '2', 'primary_key': 'True'}),
65
+            'iso_3166_1_a3': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'db_index': 'True'}),
66
+            'iso_3166_1_numeric': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'db_index': 'True'}),
67
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
68
+            'printable_name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
69
+        },
70
+        'address.useraddress': {
71
+            'Meta': {'ordering': "['-num_orders']", 'object_name': 'UserAddress'},
72
+            'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['address.Country']"}),
73
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
74
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
75
+            'hash': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
76
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
77
+            'is_default_for_billing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
78
+            'is_default_for_shipping': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
79
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
80
+            'line1': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
81
+            'line2': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
82
+            'line3': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
83
+            'line4': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
84
+            'notes': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
85
+            'num_orders': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
86
+            'phone_number': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
87
+            'postcode': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
88
+            'search_text': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
89
+            'state': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
90
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
91
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'addresses'", 'to': "orm['{0}']".format(AUTH_USER_MODEL)})
92
+        },
93
+        'auth.group': {
94
+            'Meta': {'object_name': 'Group'},
95
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
96
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
97
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
98
+        },
99
+        'auth.permission': {
100
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
101
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
102
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
103
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
104
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
105
+        },
106
+        AUTH_USER_MODEL: {
107
+            'Meta': {'object_name': AUTH_USER_MODEL_NAME},
108
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
109
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
110
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
111
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
112
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
113
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
114
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
115
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
116
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
117
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
118
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
119
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
120
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
121
+        },
122
+        'contenttypes.contenttype': {
123
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
124
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
125
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
126
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
127
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
128
+        }
129
+    }
130
+
131
+    complete_apps = ['address']

oscar/apps/address/migrations/0002_auto__chg_field_useraddress_postcode.py → oscar/apps/address/south_migrations/0002_auto__chg_field_useraddress_postcode.py View File


oscar/apps/address/migrations/0003_auto__add_field_country_display_order.py → oscar/apps/address/south_migrations/0003_auto__add_field_country_display_order.py View File


oscar/apps/address/migrations/0004_convert_is_highlighted.py → oscar/apps/address/south_migrations/0004_convert_is_highlighted.py View File


oscar/apps/address/migrations/0005_auto__del_field_country_is_highlighted.py → oscar/apps/address/south_migrations/0005_auto__del_field_country_is_highlighted.py View File


oscar/apps/address/migrations/0006_auto__add_unique_useraddress_hash_user.py → oscar/apps/address/south_migrations/0006_auto__add_unique_useraddress_hash_user.py View File


oscar/apps/address/migrations/0007_auto__chg_field_useraddress_postcode.py → oscar/apps/address/south_migrations/0007_auto__chg_field_useraddress_postcode.py View File


oscar/apps/address/migrations/0008_auto__chg_field_useraddress_phone_number.py → oscar/apps/address/south_migrations/0008_auto__chg_field_useraddress_phone_number.py View File


oscar/apps/address/migrations/0009_no_null_in_charfields.py → oscar/apps/address/south_migrations/0009_no_null_in_charfields.py View File


oscar/apps/address/migrations/0010_auto__chg_field_useraddress_first_name__chg_field_useraddress_title__c.py → oscar/apps/address/south_migrations/0010_auto__chg_field_useraddress_first_name__chg_field_useraddress_title__c.py View File


oscar/apps/address/migrations/0011_auto__chg_field_useraddress_search_text.py → oscar/apps/address/south_migrations/0011_auto__chg_field_useraddress_search_text.py View File


oscar/apps/address/migrations/0012_auto__del_index_country_iso_3166_1_a3__chg_field_country_iso_3166_1_nu.py → oscar/apps/address/south_migrations/0012_auto__del_index_country_iso_3166_1_a3__chg_field_country_iso_3166_1_nu.py View File


oscar/apps/checkout/migrations/__init__.py → oscar/apps/address/south_migrations/__init__.py View File


+ 1
- 0
oscar/apps/analytics/__init__.py View File

@@ -0,0 +1 @@
1
+default_app_config = 'oscar.apps.analytics.config.AnalyticsConfig'

+ 11
- 3
oscar/apps/analytics/abstract_models.py View File

@@ -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.
@@ -29,11 +31,12 @@ class AbstractProductRecord(models.Model):
29 31
 
30 32
     class Meta:
31 33
         abstract = True
34
+        app_label = 'analytics'
32 35
         ordering = ['-num_purchases']
33 36
         verbose_name = _('Product record')
34 37
         verbose_name_plural = _('Product records')
35 38
 
36
-    def __unicode__(self):
39
+    def __str__(self):
37 40
         return _("Record for '%s'") % self.product
38 41
 
39 42
 
@@ -64,10 +67,12 @@ class AbstractUserRecord(models.Model):
64 67
 
65 68
     class Meta:
66 69
         abstract = True
70
+        app_label = 'analytics'
67 71
         verbose_name = _('User record')
68 72
         verbose_name_plural = _('User records')
69 73
 
70 74
 
75
+@python_2_unicode_compatible
71 76
 class AbstractUserProductView(models.Model):
72 77
 
73 78
     user = models.ForeignKey(AUTH_USER_MODEL, verbose_name=_("User"))
@@ -76,14 +81,16 @@ class AbstractUserProductView(models.Model):
76 81
 
77 82
     class Meta:
78 83
         abstract = True
84
+        app_label = 'analytics'
79 85
         verbose_name = _('User product view')
80 86
         verbose_name_plural = _('User product views')
81 87
 
82
-    def __unicode__(self):
88
+    def __str__(self):
83 89
         return _("%(user)s viewed '%(product)s'") % {
84 90
             'user': self.user, 'product': self.product}
85 91
 
86 92
 
93
+@python_2_unicode_compatible
87 94
 class AbstractUserSearch(models.Model):
88 95
 
89 96
     user = models.ForeignKey(AUTH_USER_MODEL, verbose_name=_("User"))
@@ -92,10 +99,11 @@ class AbstractUserSearch(models.Model):
92 99
 
93 100
     class Meta:
94 101
         abstract = True
102
+        app_label = 'analytics'
95 103
         verbose_name = _("User search query")
96 104
         verbose_name_plural = _("User search queries")
97 105
 
98
-    def __unicode__(self):
106
+    def __str__(self):
99 107
         return _("%(user)s searched for '%(query)s'") % {
100 108
             'user': self.user,
101 109
             'query': self.query}

+ 11
- 0
oscar/apps/analytics/config.py View File

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

+ 30
- 254
oscar/apps/analytics/migrations/0001_initial.py View File

@@ -1,254 +1,30 @@
1
-# encoding: utf-8
2
-import datetime
3
-from south.db import db
4
-from south.v2 import SchemaMigration
5
-from django.db import models
6
-from oscar.core.compat import AUTH_USER_MODEL, AUTH_USER_MODEL_NAME
7
-
8
-class Migration(SchemaMigration):
9
-    depends_on = (
10
-        ('customer', '0001_initial'),
11
-        ('catalogue', '0001_initial'),
12
-    )
13
-
14
-    def forwards(self, orm):
15
-        
16
-        # Adding model 'ProductRecord'
17
-        db.create_table('analytics_productrecord', (
18
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
19
-            ('product', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['catalogue.Product'], unique=True)),
20
-            ('num_views', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
21
-            ('num_basket_additions', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
22
-            ('num_purchases', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, db_index=True)),
23
-            ('score', self.gf('django.db.models.fields.FloatField')(default=0.0)),
24
-        ))
25
-        db.send_create_signal('analytics', ['ProductRecord'])
26
-
27
-        # Adding model 'UserRecord'
28
-        db.create_table('analytics_userrecord', (
29
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
30
-            ('user', self.gf('django.db.models.fields.related.OneToOneField')(to=orm[AUTH_USER_MODEL], unique=True)),
31
-            ('num_product_views', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
32
-            ('num_basket_additions', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
33
-            ('num_orders', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, db_index=True)),
34
-            ('num_order_lines', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, db_index=True)),
35
-            ('num_order_items', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, db_index=True)),
36
-            ('total_spent', self.gf('django.db.models.fields.DecimalField')(default='0.00', max_digits=12, decimal_places=2)),
37
-            ('date_last_order', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
38
-        ))
39
-        db.send_create_signal('analytics', ['UserRecord'])
40
-
41
-        # Adding model 'UserProductView'
42
-        db.create_table('analytics_userproductview', (
43
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
44
-            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[AUTH_USER_MODEL])),
45
-            ('product', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Product'])),
46
-            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
47
-        ))
48
-        db.send_create_signal('analytics', ['UserProductView'])
49
-
50
-        # Adding model 'UserSearch'
51
-        db.create_table('analytics_usersearch', (
52
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
53
-            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[AUTH_USER_MODEL])),
54
-            ('query', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
55
-            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
56
-        ))
57
-        db.send_create_signal('analytics', ['UserSearch'])
58
-
59
-
60
-    def backwards(self, orm):
61
-        
62
-        # Deleting model 'ProductRecord'
63
-        db.delete_table('analytics_productrecord')
64
-
65
-        # Deleting model 'UserRecord'
66
-        db.delete_table('analytics_userrecord')
67
-
68
-        # Deleting model 'UserProductView'
69
-        db.delete_table('analytics_userproductview')
70
-
71
-        # Deleting model 'UserSearch'
72
-        db.delete_table('analytics_usersearch')
73
-
74
-
75
-    models = {
76
-        'analytics.productrecord': {
77
-            'Meta': {'ordering': "['-num_purchases']", 'object_name': 'ProductRecord'},
78
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
79
-            'num_basket_additions': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
80
-            'num_purchases': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'}),
81
-            'num_views': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
82
-            'product': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['catalogue.Product']", 'unique': 'True'}),
83
-            'score': ('django.db.models.fields.FloatField', [], {'default': '0.0'})
84
-        },
85
-        'analytics.userproductview': {
86
-            'Meta': {'object_name': 'UserProductView'},
87
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
88
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
89
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"}),
90
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['{0}']".format(AUTH_USER_MODEL)})
91
-        },
92
-        'analytics.userrecord': {
93
-            'Meta': {'object_name': 'UserRecord'},
94
-            'date_last_order': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
95
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
96
-            'num_basket_additions': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
97
-            'num_order_items': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'}),
98
-            'num_order_lines': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'}),
99
-            'num_orders': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'}),
100
-            'num_product_views': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
101
-            'total_spent': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '12', 'decimal_places': '2'}),
102
-            'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['{0}']".format(AUTH_USER_MODEL), 'unique': 'True'})
103
-        },
104
-        'analytics.usersearch': {
105
-            'Meta': {'object_name': 'UserSearch'},
106
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
107
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
108
-            'query': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
109
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['{0}']".format(AUTH_USER_MODEL)})
110
-        },
111
-        'auth.group': {
112
-            'Meta': {'object_name': 'Group'},
113
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
114
-            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
115
-            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
116
-        },
117
-        'auth.permission': {
118
-            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
119
-            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
120
-            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
121
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
122
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
123
-        },
124
-        AUTH_USER_MODEL: {
125
-            'Meta': {'object_name': AUTH_USER_MODEL_NAME},
126
-            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
127
-            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
128
-            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
129
-            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
130
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
131
-            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
132
-            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
133
-            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
134
-            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
135
-            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
136
-            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
137
-            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
138
-            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
139
-        },
140
-        'catalogue.attributeentity': {
141
-            'Meta': {'object_name': 'AttributeEntity'},
142
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
143
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
144
-            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
145
-            'type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entities'", 'to': "orm['catalogue.AttributeEntityType']"})
146
-        },
147
-        'catalogue.attributeentitytype': {
148
-            'Meta': {'object_name': 'AttributeEntityType'},
149
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
150
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
151
-            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'})
152
-        },
153
-        'catalogue.attributeoption': {
154
-            'Meta': {'object_name': 'AttributeOption'},
155
-            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'options'", 'to': "orm['catalogue.AttributeOptionGroup']"}),
156
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
157
-            'option': ('django.db.models.fields.CharField', [], {'max_length': '255'})
158
-        },
159
-        'catalogue.attributeoptiongroup': {
160
-            'Meta': {'object_name': 'AttributeOptionGroup'},
161
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
162
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
163
-        },
164
-        'catalogue.category': {
165
-            'Meta': {'ordering': "['name']", 'object_name': 'Category'},
166
-            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
167
-            'full_name': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'db_index': 'True'}),
168
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
169
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
170
-            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
171
-            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
172
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '1024', 'db_index': 'True'})
173
-        },
174
-        'catalogue.option': {
175
-            'Meta': {'object_name': 'Option'},
176
-            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
177
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
178
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
179
-            'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
180
-        },
181
-        'catalogue.product': {
182
-            'Meta': {'ordering': "['-date_created']", 'object_name': 'Product'},
183
-            'attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.ProductAttribute']", 'through': "orm['catalogue.ProductAttributeValue']", 'symmetrical': 'False'}),
184
-            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Category']", 'through': "orm['catalogue.ProductCategory']", 'symmetrical': 'False'}),
185
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
186
-            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
187
-            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
188
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
189
-            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'variants'", 'null': 'True', 'to': "orm['catalogue.Product']"}),
190
-            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductClass']", 'null': 'True'}),
191
-            'product_options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
192
-            'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Product']", 'symmetrical': 'False', 'through': "orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
193
-            'related_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'relations'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
194
-            'score': ('django.db.models.fields.FloatField', [], {'default': '0.0', 'db_index': 'True'}),
195
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
196
-            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
197
-            'upc': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'})
198
-        },
199
-        'catalogue.productattribute': {
200
-            'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
201
-            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
202
-            'entity_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntityType']", 'null': 'True', 'blank': 'True'}),
203
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
204
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
205
-            'option_group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOptionGroup']", 'null': 'True', 'blank': 'True'}),
206
-            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attributes'", 'null': 'True', 'to': "orm['catalogue.ProductClass']"}),
207
-            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
208
-            'type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '20'})
209
-        },
210
-        'catalogue.productattributevalue': {
211
-            'Meta': {'object_name': 'ProductAttributeValue'},
212
-            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductAttribute']"}),
213
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
214
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"}),
215
-            'value_boolean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
216
-            'value_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
217
-            'value_entity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntity']", 'null': 'True', 'blank': 'True'}),
218
-            'value_float': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
219
-            'value_integer': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
220
-            'value_option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOption']", 'null': 'True', 'blank': 'True'}),
221
-            'value_richtext': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
222
-            'value_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
223
-        },
224
-        'catalogue.productcategory': {
225
-            'Meta': {'ordering': "['-is_canonical']", 'object_name': 'ProductCategory'},
226
-            'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Category']"}),
227
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
228
-            'is_canonical': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
229
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
230
-        },
231
-        'catalogue.productclass': {
232
-            'Meta': {'ordering': "['name']", 'object_name': 'ProductClass'},
233
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
234
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
235
-            'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
236
-            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'})
237
-        },
238
-        'catalogue.productrecommendation': {
239
-            'Meta': {'object_name': 'ProductRecommendation'},
240
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
241
-            'primary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'primary_recommendations'", 'to': "orm['catalogue.Product']"}),
242
-            'ranking': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
243
-            'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
244
-        },
245
-        'contenttypes.contenttype': {
246
-            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
247
-            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
248
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
249
-            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
250
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
251
-        }
252
-    }
253
-
254
-    complete_apps = ['analytics']
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
+    ]
11
+
12
+    operations = [
13
+        migrations.CreateModel(
14
+            name='ProductRecord',
15
+            fields=[
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
+            ],
22
+            options={
23
+                'verbose_name': 'Product record',
24
+                'verbose_name_plural': 'Product records',
25
+                'ordering': ['-num_purchases'],
26
+                'abstract': False,
27
+            },
28
+            bases=(models.Model,),
29
+        ),
30
+    ]

+ 74
- 0
oscar/apps/analytics/migrations/0002_auto_20140805_1510.py View File

@@ -0,0 +1,74 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
3
+
4
+from django.db import models, migrations
5
+from django.conf import settings
6
+from decimal import Decimal
7
+
8
+
9
+class Migration(migrations.Migration):
10
+
11
+    dependencies = [
12
+        ('catalogue', '0001_initial'),
13
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+        ('analytics', '0001_initial'),
15
+    ]
16
+
17
+    operations = [
18
+        migrations.AddField(
19
+            model_name='productrecord',
20
+            name='product',
21
+            field=models.OneToOneField(verbose_name='Product', to='catalogue.Product'),
22
+            preserve_default=True,
23
+        ),
24
+        migrations.CreateModel(
25
+            name='UserProductView',
26
+            fields=[
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
+                ('product', models.ForeignKey(verbose_name='Product', to='catalogue.Product')),
30
+                ('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL)),
31
+            ],
32
+            options={
33
+                'verbose_name': 'User product view',
34
+                'verbose_name_plural': 'User product views',
35
+                'abstract': False,
36
+            },
37
+            bases=(models.Model,),
38
+        ),
39
+        migrations.CreateModel(
40
+            name='UserRecord',
41
+            fields=[
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
+                ('user', models.OneToOneField(verbose_name='User', to=settings.AUTH_USER_MODEL)),
51
+            ],
52
+            options={
53
+                'verbose_name': 'User record',
54
+                'verbose_name_plural': 'User records',
55
+                'abstract': False,
56
+            },
57
+            bases=(models.Model,),
58
+        ),
59
+        migrations.CreateModel(
60
+            name='UserSearch',
61
+            fields=[
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
+                ('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL)),
66
+            ],
67
+            options={
68
+                'verbose_name': 'User search query',
69
+                'verbose_name_plural': 'User search queries',
70
+                'abstract': False,
71
+            },
72
+            bases=(models.Model,),
73
+        ),
74
+    ]

+ 17
- 9
oscar/apps/analytics/models.py View File

@@ -1,22 +1,30 @@
1
+import django
2
+
3
+from oscar.core.loading import is_model_registered
1 4
 from oscar.apps.analytics.abstract_models import (
2 5
     AbstractProductRecord, AbstractUserRecord,
3 6
     AbstractUserProductView, AbstractUserSearch)
4 7
 
5 8
 
6
-class ProductRecord(AbstractProductRecord):
7
-    pass
9
+if not is_model_registered('analytics', 'ProductRecord'):
10
+    class ProductRecord(AbstractProductRecord):
11
+        pass
8 12
 
9 13
 
10
-class UserRecord(AbstractUserRecord):
11
-    pass
14
+if not is_model_registered('analytics', 'UserRecord'):
15
+    class UserRecord(AbstractUserRecord):
16
+        pass
12 17
 
13 18
 
14
-class UserProductView(AbstractUserProductView):
15
-    pass
19
+if not is_model_registered('analytics', 'UserProductView'):
20
+    class UserProductView(AbstractUserProductView):
21
+        pass
16 22
 
17 23
 
18
-class UserSearch(AbstractUserSearch):
19
-    pass
24
+if not is_model_registered('analytics', 'UserSearch'):
25
+    class UserSearch(AbstractUserSearch):
26
+        pass
20 27
 
21 28
 
22
-from .receivers import *  # noqa
29
+if django.VERSION < (1, 7):
30
+    from . import receivers  # noqa

+ 254
- 0
oscar/apps/analytics/south_migrations/0001_initial.py View File

@@ -0,0 +1,254 @@
1
+# encoding: utf-8
2
+import datetime
3
+from south.db import db
4
+from south.v2 import SchemaMigration
5
+from django.db import models
6
+from oscar.core.compat import AUTH_USER_MODEL, AUTH_USER_MODEL_NAME
7
+
8
+class Migration(SchemaMigration):
9
+    depends_on = (
10
+        ('customer', '0001_initial'),
11
+        ('catalogue', '0001_initial'),
12
+    )
13
+
14
+    def forwards(self, orm):
15
+        
16
+        # Adding model 'ProductRecord'
17
+        db.create_table('analytics_productrecord', (
18
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
19
+            ('product', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['catalogue.Product'], unique=True)),
20
+            ('num_views', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
21
+            ('num_basket_additions', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
22
+            ('num_purchases', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, db_index=True)),
23
+            ('score', self.gf('django.db.models.fields.FloatField')(default=0.0)),
24
+        ))
25
+        db.send_create_signal('analytics', ['ProductRecord'])
26
+
27
+        # Adding model 'UserRecord'
28
+        db.create_table('analytics_userrecord', (
29
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
30
+            ('user', self.gf('django.db.models.fields.related.OneToOneField')(to=orm[AUTH_USER_MODEL], unique=True)),
31
+            ('num_product_views', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
32
+            ('num_basket_additions', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
33
+            ('num_orders', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, db_index=True)),
34
+            ('num_order_lines', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, db_index=True)),
35
+            ('num_order_items', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, db_index=True)),
36
+            ('total_spent', self.gf('django.db.models.fields.DecimalField')(default='0.00', max_digits=12, decimal_places=2)),
37
+            ('date_last_order', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
38
+        ))
39
+        db.send_create_signal('analytics', ['UserRecord'])
40
+
41
+        # Adding model 'UserProductView'
42
+        db.create_table('analytics_userproductview', (
43
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
44
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[AUTH_USER_MODEL])),
45
+            ('product', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Product'])),
46
+            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
47
+        ))
48
+        db.send_create_signal('analytics', ['UserProductView'])
49
+
50
+        # Adding model 'UserSearch'
51
+        db.create_table('analytics_usersearch', (
52
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
53
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[AUTH_USER_MODEL])),
54
+            ('query', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
55
+            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
56
+        ))
57
+        db.send_create_signal('analytics', ['UserSearch'])
58
+
59
+
60
+    def backwards(self, orm):
61
+        
62
+        # Deleting model 'ProductRecord'
63
+        db.delete_table('analytics_productrecord')
64
+
65
+        # Deleting model 'UserRecord'
66
+        db.delete_table('analytics_userrecord')
67
+
68
+        # Deleting model 'UserProductView'
69
+        db.delete_table('analytics_userproductview')
70
+
71
+        # Deleting model 'UserSearch'
72
+        db.delete_table('analytics_usersearch')
73
+
74
+
75
+    models = {
76
+        'analytics.productrecord': {
77
+            'Meta': {'ordering': "['-num_purchases']", 'object_name': 'ProductRecord'},
78
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
79
+            'num_basket_additions': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
80
+            'num_purchases': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'}),
81
+            'num_views': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
82
+            'product': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['catalogue.Product']", 'unique': 'True'}),
83
+            'score': ('django.db.models.fields.FloatField', [], {'default': '0.0'})
84
+        },
85
+        'analytics.userproductview': {
86
+            'Meta': {'object_name': 'UserProductView'},
87
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
88
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
89
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"}),
90
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['{0}']".format(AUTH_USER_MODEL)})
91
+        },
92
+        'analytics.userrecord': {
93
+            'Meta': {'object_name': 'UserRecord'},
94
+            'date_last_order': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
95
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
96
+            'num_basket_additions': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
97
+            'num_order_items': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'}),
98
+            'num_order_lines': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'}),
99
+            'num_orders': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'}),
100
+            'num_product_views': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
101
+            'total_spent': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '12', 'decimal_places': '2'}),
102
+            'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['{0}']".format(AUTH_USER_MODEL), 'unique': 'True'})
103
+        },
104
+        'analytics.usersearch': {
105
+            'Meta': {'object_name': 'UserSearch'},
106
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
107
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
108
+            'query': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
109
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['{0}']".format(AUTH_USER_MODEL)})
110
+        },
111
+        'auth.group': {
112
+            'Meta': {'object_name': 'Group'},
113
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
114
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
115
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
116
+        },
117
+        'auth.permission': {
118
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
119
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
120
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
121
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
122
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
123
+        },
124
+        AUTH_USER_MODEL: {
125
+            'Meta': {'object_name': AUTH_USER_MODEL_NAME},
126
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
127
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
128
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
129
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
130
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
131
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
132
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
133
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
134
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
135
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
136
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
137
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
138
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
139
+        },
140
+        'catalogue.attributeentity': {
141
+            'Meta': {'object_name': 'AttributeEntity'},
142
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
143
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
144
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
145
+            'type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entities'", 'to': "orm['catalogue.AttributeEntityType']"})
146
+        },
147
+        'catalogue.attributeentitytype': {
148
+            'Meta': {'object_name': 'AttributeEntityType'},
149
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
150
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
151
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'})
152
+        },
153
+        'catalogue.attributeoption': {
154
+            'Meta': {'object_name': 'AttributeOption'},
155
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'options'", 'to': "orm['catalogue.AttributeOptionGroup']"}),
156
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
157
+            'option': ('django.db.models.fields.CharField', [], {'max_length': '255'})
158
+        },
159
+        'catalogue.attributeoptiongroup': {
160
+            'Meta': {'object_name': 'AttributeOptionGroup'},
161
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
162
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
163
+        },
164
+        'catalogue.category': {
165
+            'Meta': {'ordering': "['name']", 'object_name': 'Category'},
166
+            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
167
+            'full_name': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'db_index': 'True'}),
168
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
169
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
170
+            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
171
+            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
172
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '1024', 'db_index': 'True'})
173
+        },
174
+        'catalogue.option': {
175
+            'Meta': {'object_name': 'Option'},
176
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
177
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
178
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
179
+            'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
180
+        },
181
+        'catalogue.product': {
182
+            'Meta': {'ordering': "['-date_created']", 'object_name': 'Product'},
183
+            'attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.ProductAttribute']", 'through': "orm['catalogue.ProductAttributeValue']", 'symmetrical': 'False'}),
184
+            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Category']", 'through': "orm['catalogue.ProductCategory']", 'symmetrical': 'False'}),
185
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
186
+            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
187
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
188
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
189
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'variants'", 'null': 'True', 'to': "orm['catalogue.Product']"}),
190
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductClass']", 'null': 'True'}),
191
+            'product_options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
192
+            'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Product']", 'symmetrical': 'False', 'through': "orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
193
+            'related_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'relations'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
194
+            'score': ('django.db.models.fields.FloatField', [], {'default': '0.0', 'db_index': 'True'}),
195
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
196
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
197
+            'upc': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'})
198
+        },
199
+        'catalogue.productattribute': {
200
+            'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
201
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
202
+            'entity_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntityType']", 'null': 'True', 'blank': 'True'}),
203
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
204
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
205
+            'option_group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOptionGroup']", 'null': 'True', 'blank': 'True'}),
206
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attributes'", 'null': 'True', 'to': "orm['catalogue.ProductClass']"}),
207
+            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
208
+            'type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '20'})
209
+        },
210
+        'catalogue.productattributevalue': {
211
+            'Meta': {'object_name': 'ProductAttributeValue'},
212
+            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductAttribute']"}),
213
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
214
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"}),
215
+            'value_boolean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
216
+            'value_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
217
+            'value_entity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntity']", 'null': 'True', 'blank': 'True'}),
218
+            'value_float': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
219
+            'value_integer': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
220
+            'value_option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOption']", 'null': 'True', 'blank': 'True'}),
221
+            'value_richtext': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
222
+            'value_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
223
+        },
224
+        'catalogue.productcategory': {
225
+            'Meta': {'ordering': "['-is_canonical']", 'object_name': 'ProductCategory'},
226
+            'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Category']"}),
227
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
228
+            'is_canonical': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
229
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
230
+        },
231
+        'catalogue.productclass': {
232
+            'Meta': {'ordering': "['name']", 'object_name': 'ProductClass'},
233
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
234
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
235
+            'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
236
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'})
237
+        },
238
+        'catalogue.productrecommendation': {
239
+            'Meta': {'object_name': 'ProductRecommendation'},
240
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
241
+            'primary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'primary_recommendations'", 'to': "orm['catalogue.Product']"}),
242
+            'ranking': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
243
+            'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
244
+        },
245
+        'contenttypes.contenttype': {
246
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
247
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
248
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
249
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
250
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
251
+        }
252
+    }
253
+
254
+    complete_apps = ['analytics']

oscar/apps/dashboard/ranges/migrations/__init__.py → oscar/apps/analytics/south_migrations/__init__.py View File


+ 1
- 0
oscar/apps/basket/__init__.py View File

@@ -0,0 +1 @@
1
+default_app_config = 'oscar.apps.basket.config.BasketConfig'

+ 8
- 2
oscar/apps/basket/abstract_models.py View File

@@ -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
@@ -55,6 +57,7 @@ class AbstractBasket(models.Model):
55 57
 
56 58
     class Meta:
57 59
         abstract = True
60
+        app_label = 'basket'
58 61
         verbose_name = _('Basket')
59 62
         verbose_name_plural = _('Baskets')
60 63
 
@@ -73,7 +76,7 @@ class AbstractBasket(models.Model):
73 76
         self._lines = None
74 77
         self.offer_applications = results.OfferApplications()
75 78
 
76
-    def __unicode__(self):
79
+    def __str__(self):
77 80
         return _(
78 81
             u"%(status)s basket (owner: %(owner)s, lines: %(num_lines)d)") \
79 82
             % {'status': self.status,
@@ -537,6 +540,7 @@ class AbstractBasket(models.Model):
537 540
             return 0
538 541
 
539 542
 
543
+@python_2_unicode_compatible
540 544
 class AbstractLine(models.Model):
541 545
     """
542 546
     A line of a basket (product and a quantity)
@@ -584,11 +588,12 @@ class AbstractLine(models.Model):
584 588
 
585 589
     class Meta:
586 590
         abstract = True
591
+        app_label = 'basket'
587 592
         unique_together = ("basket", "line_reference")
588 593
         verbose_name = _('Basket line')
589 594
         verbose_name_plural = _('Basket lines')
590 595
 
591
-    def __unicode__(self):
596
+    def __str__(self):
592 597
         return _(
593 598
             u"Basket #%(basket_id)d, Product #%(product_id)d, quantity"
594 599
             u" %(quantity)d") % {'basket_id': self.basket.pk,
@@ -831,5 +836,6 @@ class AbstractLineAttribute(models.Model):
831 836
 
832 837
     class Meta:
833 838
         abstract = True
839
+        app_label = 'basket'
834 840
         verbose_name = _('Line attribute')
835 841
         verbose_name_plural = _('Line attributes')

+ 8
- 0
oscar/apps/basket/config.py View File

@@ -0,0 +1,8 @@
1
+from django.apps import AppConfig
2
+from django.utils.translation import ugettext_lazy as _
3
+
4
+
5
+class BasketConfig(AppConfig):
6
+    label = 'basket'
7
+    name = 'oscar.apps.basket'
8
+    verbose_name = _('Basket')

+ 5
- 5
oscar/apps/basket/forms.py View File

@@ -135,11 +135,11 @@ class AddToBasketForm(forms.Form):
135 135
 
136 136
     def __init__(self, basket, product, *args, **kwargs):
137 137
         # Note, the product passed in here isn't necessarily the product being
138
-        # added to the basket. For group products, it is the *parent* product
138
+        # added to the basket. For child products, it is the *parent* product
139 139
         # that gets passed to the form. An optional product_id param is passed
140 140
         # to indicate the ID of the child product being added to the basket.
141 141
         self.basket = basket
142
-        self.base_product = product
142
+        self.parent_product = product
143 143
 
144 144
         super(AddToBasketForm, self).__init__(*args, **kwargs)
145 145
 
@@ -200,7 +200,7 @@ class AddToBasketForm(forms.Form):
200 200
 
201 201
     def clean_child_id(self):
202 202
         try:
203
-            child = self.base_product.children.get(
203
+            child = self.parent_product.children.get(
204 204
                 id=self.cleaned_data['child_id'])
205 205
         except Product.DoesNotExist:
206 206
             raise forms.ValidationError(
@@ -235,7 +235,7 @@ class AddToBasketForm(forms.Form):
235 235
         """
236 236
         # Note, the child product attribute is saved in the clean_child_id
237 237
         # method
238
-        return getattr(self, 'child_product', self.base_product)
238
+        return getattr(self, 'child_product', self.parent_product)
239 239
 
240 240
     def clean(self):
241 241
         info = self.basket.strategy.fetch_for_product(self.product)
@@ -265,7 +265,7 @@ class AddToBasketForm(forms.Form):
265 265
         Return submitted options in a clean format
266 266
         """
267 267
         options = []
268
-        for option in self.base_product.options:
268
+        for option in self.parent_product.options:
269 269
             if option.code in self.cleaned_data:
270 270
                 options.append({
271 271
                     'option': option,

+ 16
- 7
oscar/apps/basket/middleware.py View File

@@ -70,11 +70,12 @@ class BasketMiddleware(object):
70 70
                 and request.basket._wrapped is empty):
71 71
             return response
72 72
 
73
+        cookie_key = self.get_cookie_key(request)
73 74
         # Check if we need to set a cookie. If the cookies is already available
74 75
         # but is set in the cookies_to_delete list then we need to re-set it.
75 76
         has_basket_cookie = (
76
-            settings.OSCAR_BASKET_COOKIE_OPEN in request.COOKIES
77
-            and settings.OSCAR_BASKET_COOKIE_OPEN not in cookies_to_delete)
77
+            cookie_key in request.COOKIES
78
+            and cookie_key not in cookies_to_delete)
78 79
 
79 80
         # If a basket has had products added to it, but the user is anonymous
80 81
         # then we need to assign it to a cookie
@@ -82,10 +83,19 @@ class BasketMiddleware(object):
82 83
                 and not has_basket_cookie):
83 84
             cookie = self.get_basket_hash(request.basket.id)
84 85
             response.set_cookie(
85
-                settings.OSCAR_BASKET_COOKIE_OPEN, cookie,
86
+                cookie_key, cookie,
86 87
                 max_age=settings.OSCAR_BASKET_COOKIE_LIFETIME, httponly=True)
87 88
         return response
88 89
 
90
+    def get_cookie_key(self, request):
91
+        """
92
+        Returns the cookie name to use for storing a cookie basket.
93
+
94
+        The method serves as a useful hook in multi-site scenarios where
95
+        different baskets might be needed.
96
+        """
97
+        return settings.OSCAR_BASKET_COOKIE_OPEN
98
+
89 99
     def process_template_response(self, request, response):
90 100
         if hasattr(response, 'context_data'):
91 101
             if response.context_data is None:
@@ -114,8 +124,8 @@ class BasketMiddleware(object):
114 124
             return request._basket_cache
115 125
 
116 126
         manager = Basket.open
117
-        cookie_basket = self.get_cookie_basket(
118
-            settings.OSCAR_BASKET_COOKIE_OPEN, request, manager)
127
+        cookie_key = self.get_cookie_key(request)
128
+        cookie_basket = self.get_cookie_basket(cookie_key, request, manager)
119 129
 
120 130
         if hasattr(request, 'user') and request.user.is_authenticated():
121 131
             # Signed-in user: if they have a cookie basket too, it means
@@ -137,8 +147,7 @@ class BasketMiddleware(object):
137 147
 
138 148
             if cookie_basket:
139 149
                 self.merge_baskets(basket, cookie_basket)
140
-                request.cookies_to_delete.append(
141
-                    settings.OSCAR_BASKET_COOKIE_OPEN)
150
+                request.cookies_to_delete.append(cookie_key)
142 151
 
143 152
         elif cookie_basket:
144 153
             # Anonymous user with a basket tied to the cookie

+ 32
- 302
oscar/apps/basket/migrations/0001_initial.py View File

@@ -1,302 +1,32 @@
1
-# encoding: utf-8
2
-import datetime
3
-from south.db import db
4
-from south.v2 import SchemaMigration
5
-from django.db import models
6
-from oscar.core.compat import AUTH_USER_MODEL, AUTH_USER_MODEL_NAME
7
-
8
-class Migration(SchemaMigration):
9
-    depends_on = (
10
-        ('catalogue', '0001_initial'),
11
-        ('voucher', '0001_initial'),
12
-        ('offer', '0001_initial'),
13
-    )
14
-
15
-    def forwards(self, orm):
16
-        
17
-        # Adding model 'Basket'
18
-        db.create_table('basket_basket', (
19
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
20
-            ('owner', self.gf('django.db.models.fields.related.ForeignKey')(related_name='baskets', null=True, to=orm[AUTH_USER_MODEL])),
21
-            ('status', self.gf('django.db.models.fields.CharField')(default='Open', max_length=128)),
22
-            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
23
-            ('date_merged', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
24
-            ('date_submitted', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
25
-        ))
26
-        db.send_create_signal('basket', ['Basket'])
27
-
28
-        # Adding M2M table for field vouchers on 'Basket'
29
-        db.create_table('basket_basket_vouchers', (
30
-            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
31
-            ('basket', models.ForeignKey(orm['basket.basket'], null=False)),
32
-            ('voucher', models.ForeignKey(orm['voucher.voucher'], null=False))
33
-        ))
34
-        db.create_unique('basket_basket_vouchers', ['basket_id', 'voucher_id'])
35
-
36
-        # Adding model 'Line'
37
-        db.create_table('basket_line', (
38
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
39
-            ('basket', self.gf('django.db.models.fields.related.ForeignKey')(related_name='lines', to=orm['basket.Basket'])),
40
-            ('line_reference', self.gf('django.db.models.fields.SlugField')(max_length=128, db_index=True)),
41
-            ('product', self.gf('django.db.models.fields.related.ForeignKey')(related_name='basket_lines', to=orm['catalogue.Product'])),
42
-            ('quantity', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)),
43
-            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
44
-        ))
45
-        db.send_create_signal('basket', ['Line'])
46
-
47
-        # Adding unique constraint on 'Line', fields ['basket', 'line_reference']
48
-        db.create_unique('basket_line', ['basket_id', 'line_reference'])
49
-
50
-        # Adding model 'LineAttribute'
51
-        db.create_table('basket_lineattribute', (
52
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
53
-            ('line', self.gf('django.db.models.fields.related.ForeignKey')(related_name='attributes', to=orm['basket.Line'])),
54
-            ('option', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Option'])),
55
-            ('value', self.gf('django.db.models.fields.CharField')(max_length=255)),
56
-        ))
57
-        db.send_create_signal('basket', ['LineAttribute'])
58
-
59
-
60
-    def backwards(self, orm):
61
-        
62
-        # Removing unique constraint on 'Line', fields ['basket', 'line_reference']
63
-        db.delete_unique('basket_line', ['basket_id', 'line_reference'])
64
-
65
-        # Deleting model 'Basket'
66
-        db.delete_table('basket_basket')
67
-
68
-        # Removing M2M table for field vouchers on 'Basket'
69
-        db.delete_table('basket_basket_vouchers')
70
-
71
-        # Deleting model 'Line'
72
-        db.delete_table('basket_line')
73
-
74
-        # Deleting model 'LineAttribute'
75
-        db.delete_table('basket_lineattribute')
76
-
77
-
78
-    models = {
79
-        'auth.group': {
80
-            'Meta': {'object_name': 'Group'},
81
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
82
-            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
83
-            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
84
-        },
85
-        'auth.permission': {
86
-            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
87
-            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
88
-            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
89
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
90
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
91
-        },
92
-        AUTH_USER_MODEL: {
93
-            'Meta': {'object_name': AUTH_USER_MODEL_NAME},
94
-            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
95
-            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
96
-            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
97
-            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
98
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
99
-            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
100
-            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
101
-            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
102
-            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
103
-            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
104
-            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
105
-            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
106
-            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
107
-        },
108
-        'basket.basket': {
109
-            'Meta': {'object_name': 'Basket'},
110
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
111
-            'date_merged': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
112
-            'date_submitted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
113
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
114
-            'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'baskets'", 'null': 'True', 'to': "orm['{0}']".format(AUTH_USER_MODEL)}),
115
-            'status': ('django.db.models.fields.CharField', [], {'default': "'Open'", 'max_length': '128'}),
116
-            'vouchers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['voucher.Voucher']", 'null': 'True', 'symmetrical': 'False'})
117
-        },
118
-        'basket.line': {
119
-            'Meta': {'unique_together': "(('basket', 'line_reference'),)", 'object_name': 'Line'},
120
-            'basket': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'lines'", 'to': "orm['basket.Basket']"}),
121
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
122
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
123
-            'line_reference': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
124
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'basket_lines'", 'to': "orm['catalogue.Product']"}),
125
-            'quantity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'})
126
-        },
127
-        'basket.lineattribute': {
128
-            'Meta': {'object_name': 'LineAttribute'},
129
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
130
-            'line': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attributes'", 'to': "orm['basket.Line']"}),
131
-            'option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Option']"}),
132
-            'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
133
-        },
134
-        'catalogue.attributeentity': {
135
-            'Meta': {'object_name': 'AttributeEntity'},
136
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
137
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
138
-            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
139
-            'type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entities'", 'to': "orm['catalogue.AttributeEntityType']"})
140
-        },
141
-        'catalogue.attributeentitytype': {
142
-            'Meta': {'object_name': 'AttributeEntityType'},
143
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
144
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
145
-            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'})
146
-        },
147
-        'catalogue.attributeoption': {
148
-            'Meta': {'object_name': 'AttributeOption'},
149
-            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'options'", 'to': "orm['catalogue.AttributeOptionGroup']"}),
150
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
151
-            'option': ('django.db.models.fields.CharField', [], {'max_length': '255'})
152
-        },
153
-        'catalogue.attributeoptiongroup': {
154
-            'Meta': {'object_name': 'AttributeOptionGroup'},
155
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
156
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
157
-        },
158
-        'catalogue.category': {
159
-            'Meta': {'ordering': "['name']", 'object_name': 'Category'},
160
-            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
161
-            'full_name': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'db_index': 'True'}),
162
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
163
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
164
-            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
165
-            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
166
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '1024', 'db_index': 'True'})
167
-        },
168
-        'catalogue.option': {
169
-            'Meta': {'object_name': 'Option'},
170
-            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
171
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
172
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
173
-            'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
174
-        },
175
-        'catalogue.product': {
176
-            'Meta': {'ordering': "['-date_created']", 'object_name': 'Product'},
177
-            'attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.ProductAttribute']", 'through': "orm['catalogue.ProductAttributeValue']", 'symmetrical': 'False'}),
178
-            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Category']", 'through': "orm['catalogue.ProductCategory']", 'symmetrical': 'False'}),
179
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
180
-            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
181
-            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
182
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
183
-            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'variants'", 'null': 'True', 'to': "orm['catalogue.Product']"}),
184
-            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductClass']", 'null': 'True'}),
185
-            'product_options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
186
-            'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Product']", 'symmetrical': 'False', 'through': "orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
187
-            'related_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'relations'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
188
-            'score': ('django.db.models.fields.FloatField', [], {'default': '0.0', 'db_index': 'True'}),
189
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
190
-            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
191
-            'upc': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'})
192
-        },
193
-        'catalogue.productattribute': {
194
-            'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
195
-            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
196
-            'entity_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntityType']", 'null': 'True', 'blank': 'True'}),
197
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
198
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
199
-            'option_group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOptionGroup']", 'null': 'True', 'blank': 'True'}),
200
-            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attributes'", 'null': 'True', 'to': "orm['catalogue.ProductClass']"}),
201
-            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
202
-            'type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '20'})
203
-        },
204
-        'catalogue.productattributevalue': {
205
-            'Meta': {'object_name': 'ProductAttributeValue'},
206
-            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductAttribute']"}),
207
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
208
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"}),
209
-            'value_boolean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
210
-            'value_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
211
-            'value_entity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntity']", 'null': 'True', 'blank': 'True'}),
212
-            'value_float': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
213
-            'value_integer': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
214
-            'value_option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOption']", 'null': 'True', 'blank': 'True'}),
215
-            'value_richtext': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
216
-            'value_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
217
-        },
218
-        'catalogue.productcategory': {
219
-            'Meta': {'ordering': "['-is_canonical']", 'object_name': 'ProductCategory'},
220
-            'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Category']"}),
221
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
222
-            'is_canonical': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
223
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
224
-        },
225
-        'catalogue.productclass': {
226
-            'Meta': {'ordering': "['name']", 'object_name': 'ProductClass'},
227
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
228
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
229
-            'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
230
-            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'})
231
-        },
232
-        'catalogue.productrecommendation': {
233
-            'Meta': {'object_name': 'ProductRecommendation'},
234
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
235
-            'primary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'primary_recommendations'", 'to': "orm['catalogue.Product']"}),
236
-            'ranking': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
237
-            'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
238
-        },
239
-        'contenttypes.contenttype': {
240
-            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
241
-            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
242
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
243
-            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
244
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
245
-        },
246
-        'offer.benefit': {
247
-            'Meta': {'object_name': 'Benefit'},
248
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
249
-            'max_affected_items': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
250
-            'range': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['offer.Range']", 'null': 'True', 'blank': 'True'}),
251
-            'type': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
252
-            'value': ('oscar.models.fields.PositiveDecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'})
253
-        },
254
-        'offer.condition': {
255
-            'Meta': {'object_name': 'Condition'},
256
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
257
-            'range': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['offer.Range']"}),
258
-            'type': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
259
-            'value': ('oscar.models.fields.PositiveDecimalField', [], {'max_digits': '12', 'decimal_places': '2'})
260
-        },
261
-        'offer.conditionaloffer': {
262
-            'Meta': {'ordering': "['-priority']", 'object_name': 'ConditionalOffer'},
263
-            'benefit': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['offer.Benefit']"}),
264
-            'condition': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['offer.Condition']"}),
265
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
266
-            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
267
-            'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
268
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
269
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
270
-            'offer_type': ('django.db.models.fields.CharField', [], {'default': "'Site'", 'max_length': '128'}),
271
-            'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
272
-            'redirect_url': ('oscar.models.fields.ExtendedURLField', [], {'max_length': '200', 'blank': 'True'}),
273
-            'start_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
274
-            'total_discount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '12', 'decimal_places': '2'})
275
-        },
276
-        'offer.range': {
277
-            'Meta': {'object_name': 'Range'},
278
-            'classes': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'classes'", 'blank': 'True', 'to': "orm['catalogue.ProductClass']"}),
279
-            'excluded_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'excludes'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
280
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
281
-            'included_categories': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'includes'", 'blank': 'True', 'to': "orm['catalogue.Category']"}),
282
-            'included_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'includes'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
283
-            'includes_all_products': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
284
-            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'})
285
-        },
286
-        'voucher.voucher': {
287
-            'Meta': {'object_name': 'Voucher'},
288
-            'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
289
-            'date_created': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
290
-            'end_date': ('django.db.models.fields.DateField', [], {}),
291
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
292
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
293
-            'num_basket_additions': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
294
-            'num_orders': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
295
-            'offers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'vouchers'", 'symmetrical': 'False', 'to': "orm['offer.ConditionalOffer']"}),
296
-            'start_date': ('django.db.models.fields.DateField', [], {}),
297
-            'total_discount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '12', 'decimal_places': '2'}),
298
-            'usage': ('django.db.models.fields.CharField', [], {'default': "'Multi-use'", 'max_length': '128'})
299
-        }
300
-    }
301
-
302
-    complete_apps = ['basket']
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
3
+
4
+from django.db import models, migrations
5
+from django.conf import settings
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    dependencies = [
11
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12
+    ]
13
+
14
+    operations = [
15
+        migrations.CreateModel(
16
+            name='Basket',
17
+            fields=[
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
+                ('owner', models.ForeignKey(verbose_name='Owner', to=settings.AUTH_USER_MODEL, null=True)),
24
+            ],
25
+            options={
26
+                'verbose_name': 'Basket',
27
+                'verbose_name_plural': 'Baskets',
28
+                'abstract': False,
29
+            },
30
+            bases=(models.Model,),
31
+        ),
32
+    ]

+ 63
- 0
oscar/apps/basket/migrations/0002_auto_20140805_1510.py View File

@@ -0,0 +1,63 @@
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
+        ('catalogue', '0001_initial'),
11
+        ('voucher', '0001_initial'),
12
+        ('basket', '0001_initial'),
13
+        ('partner', '0001_initial'),
14
+    ]
15
+
16
+    operations = [
17
+        migrations.AddField(
18
+            model_name='basket',
19
+            name='vouchers',
20
+            field=models.ManyToManyField(verbose_name='Vouchers', blank=True, to='voucher.Voucher', null=True),
21
+            preserve_default=True,
22
+        ),
23
+        migrations.CreateModel(
24
+            name='Line',
25
+            fields=[
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
+                ('basket', models.ForeignKey(verbose_name='Basket', to='basket.Basket')),
34
+                ('product', models.ForeignKey(verbose_name='Product', to='catalogue.Product')),
35
+                ('stockrecord', models.ForeignKey(to='partner.StockRecord')),
36
+            ],
37
+            options={
38
+                'verbose_name': 'Basket line',
39
+                'verbose_name_plural': 'Basket lines',
40
+                'abstract': False,
41
+            },
42
+            bases=(models.Model,),
43
+        ),
44
+        migrations.AlterUniqueTogether(
45
+            name='line',
46
+            unique_together=set([('basket', 'line_reference')]),
47
+        ),
48
+        migrations.CreateModel(
49
+            name='LineAttribute',
50
+            fields=[
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
+                ('line', models.ForeignKey(verbose_name='Line', to='basket.Line')),
54
+                ('option', models.ForeignKey(verbose_name='Option', to='catalogue.Option')),
55
+            ],
56
+            options={
57
+                'verbose_name': 'Line attribute',
58
+                'verbose_name_plural': 'Line attributes',
59
+                'abstract': False,
60
+            },
61
+            bases=(models.Model,),
62
+        ),
63
+    ]

+ 10
- 6
oscar/apps/basket/models.py View File

@@ -1,3 +1,4 @@
1
+from oscar.core.loading import is_model_registered
1 2
 from oscar.apps.basket.abstract_models import (
2 3
     AbstractBasket, AbstractLine, AbstractLineAttribute)
3 4
 
@@ -6,13 +7,16 @@ class InvalidBasketLineError(Exception):
6 7
     pass
7 8
 
8 9
 
9
-class Basket(AbstractBasket):
10
-    pass
10
+if not is_model_registered('basket', 'Basket'):
11
+    class Basket(AbstractBasket):
12
+        pass
11 13
 
12 14
 
13
-class Line(AbstractLine):
14
-    pass
15
+if not is_model_registered('basket', 'Line'):
16
+    class Line(AbstractLine):
17
+        pass
15 18
 
16 19
 
17
-class LineAttribute(AbstractLineAttribute):
18
-    pass
20
+if not is_model_registered('basket', 'LineAttribute'):
21
+    class LineAttribute(AbstractLineAttribute):
22
+        pass

+ 302
- 0
oscar/apps/basket/south_migrations/0001_initial.py View File

@@ -0,0 +1,302 @@
1
+# encoding: utf-8
2
+import datetime
3
+from south.db import db
4
+from south.v2 import SchemaMigration
5
+from django.db import models
6
+from oscar.core.compat import AUTH_USER_MODEL, AUTH_USER_MODEL_NAME
7
+
8
+class Migration(SchemaMigration):
9
+    depends_on = (
10
+        ('catalogue', '0001_initial'),
11
+        ('voucher', '0001_initial'),
12
+        ('offer', '0001_initial'),
13
+    )
14
+
15
+    def forwards(self, orm):
16
+        
17
+        # Adding model 'Basket'
18
+        db.create_table('basket_basket', (
19
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
20
+            ('owner', self.gf('django.db.models.fields.related.ForeignKey')(related_name='baskets', null=True, to=orm[AUTH_USER_MODEL])),
21
+            ('status', self.gf('django.db.models.fields.CharField')(default='Open', max_length=128)),
22
+            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
23
+            ('date_merged', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
24
+            ('date_submitted', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
25
+        ))
26
+        db.send_create_signal('basket', ['Basket'])
27
+
28
+        # Adding M2M table for field vouchers on 'Basket'
29
+        db.create_table('basket_basket_vouchers', (
30
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
31
+            ('basket', models.ForeignKey(orm['basket.basket'], null=False)),
32
+            ('voucher', models.ForeignKey(orm['voucher.voucher'], null=False))
33
+        ))
34
+        db.create_unique('basket_basket_vouchers', ['basket_id', 'voucher_id'])
35
+
36
+        # Adding model 'Line'
37
+        db.create_table('basket_line', (
38
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
39
+            ('basket', self.gf('django.db.models.fields.related.ForeignKey')(related_name='lines', to=orm['basket.Basket'])),
40
+            ('line_reference', self.gf('django.db.models.fields.SlugField')(max_length=128, db_index=True)),
41
+            ('product', self.gf('django.db.models.fields.related.ForeignKey')(related_name='basket_lines', to=orm['catalogue.Product'])),
42
+            ('quantity', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)),
43
+            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
44
+        ))
45
+        db.send_create_signal('basket', ['Line'])
46
+
47
+        # Adding unique constraint on 'Line', fields ['basket', 'line_reference']
48
+        db.create_unique('basket_line', ['basket_id', 'line_reference'])
49
+
50
+        # Adding model 'LineAttribute'
51
+        db.create_table('basket_lineattribute', (
52
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
53
+            ('line', self.gf('django.db.models.fields.related.ForeignKey')(related_name='attributes', to=orm['basket.Line'])),
54
+            ('option', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Option'])),
55
+            ('value', self.gf('django.db.models.fields.CharField')(max_length=255)),
56
+        ))
57
+        db.send_create_signal('basket', ['LineAttribute'])
58
+
59
+
60
+    def backwards(self, orm):
61
+        
62
+        # Removing unique constraint on 'Line', fields ['basket', 'line_reference']
63
+        db.delete_unique('basket_line', ['basket_id', 'line_reference'])
64
+
65
+        # Deleting model 'Basket'
66
+        db.delete_table('basket_basket')
67
+
68
+        # Removing M2M table for field vouchers on 'Basket'
69
+        db.delete_table('basket_basket_vouchers')
70
+
71
+        # Deleting model 'Line'
72
+        db.delete_table('basket_line')
73
+
74
+        # Deleting model 'LineAttribute'
75
+        db.delete_table('basket_lineattribute')
76
+
77
+
78
+    models = {
79
+        'auth.group': {
80
+            'Meta': {'object_name': 'Group'},
81
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
82
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
83
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
84
+        },
85
+        'auth.permission': {
86
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
87
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
88
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
89
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
90
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
91
+        },
92
+        AUTH_USER_MODEL: {
93
+            'Meta': {'object_name': AUTH_USER_MODEL_NAME},
94
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
95
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
96
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
97
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
98
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
99
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
100
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
101
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
102
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
103
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
104
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
105
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
106
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
107
+        },
108
+        'basket.basket': {
109
+            'Meta': {'object_name': 'Basket'},
110
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
111
+            'date_merged': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
112
+            'date_submitted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
113
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
114
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'baskets'", 'null': 'True', 'to': "orm['{0}']".format(AUTH_USER_MODEL)}),
115
+            'status': ('django.db.models.fields.CharField', [], {'default': "'Open'", 'max_length': '128'}),
116
+            'vouchers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['voucher.Voucher']", 'null': 'True', 'symmetrical': 'False'})
117
+        },
118
+        'basket.line': {
119
+            'Meta': {'unique_together': "(('basket', 'line_reference'),)", 'object_name': 'Line'},
120
+            'basket': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'lines'", 'to': "orm['basket.Basket']"}),
121
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
122
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
123
+            'line_reference': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
124
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'basket_lines'", 'to': "orm['catalogue.Product']"}),
125
+            'quantity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'})
126
+        },
127
+        'basket.lineattribute': {
128
+            'Meta': {'object_name': 'LineAttribute'},
129
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
130
+            'line': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attributes'", 'to': "orm['basket.Line']"}),
131
+            'option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Option']"}),
132
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
133
+        },
134
+        'catalogue.attributeentity': {
135
+            'Meta': {'object_name': 'AttributeEntity'},
136
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
137
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
138
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
139
+            'type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entities'", 'to': "orm['catalogue.AttributeEntityType']"})
140
+        },
141
+        'catalogue.attributeentitytype': {
142
+            'Meta': {'object_name': 'AttributeEntityType'},
143
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
144
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
145
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'})
146
+        },
147
+        'catalogue.attributeoption': {
148
+            'Meta': {'object_name': 'AttributeOption'},
149
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'options'", 'to': "orm['catalogue.AttributeOptionGroup']"}),
150
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
151
+            'option': ('django.db.models.fields.CharField', [], {'max_length': '255'})
152
+        },
153
+        'catalogue.attributeoptiongroup': {
154
+            'Meta': {'object_name': 'AttributeOptionGroup'},
155
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
156
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
157
+        },
158
+        'catalogue.category': {
159
+            'Meta': {'ordering': "['name']", 'object_name': 'Category'},
160
+            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
161
+            'full_name': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'db_index': 'True'}),
162
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
163
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
164
+            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
165
+            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
166
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '1024', 'db_index': 'True'})
167
+        },
168
+        'catalogue.option': {
169
+            'Meta': {'object_name': 'Option'},
170
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
171
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
172
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
173
+            'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
174
+        },
175
+        'catalogue.product': {
176
+            'Meta': {'ordering': "['-date_created']", 'object_name': 'Product'},
177
+            'attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.ProductAttribute']", 'through': "orm['catalogue.ProductAttributeValue']", 'symmetrical': 'False'}),
178
+            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Category']", 'through': "orm['catalogue.ProductCategory']", 'symmetrical': 'False'}),
179
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
180
+            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
181
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
182
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
183
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'variants'", 'null': 'True', 'to': "orm['catalogue.Product']"}),
184
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductClass']", 'null': 'True'}),
185
+            'product_options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
186
+            'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Product']", 'symmetrical': 'False', 'through': "orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
187
+            'related_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'relations'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
188
+            'score': ('django.db.models.fields.FloatField', [], {'default': '0.0', 'db_index': 'True'}),
189
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
190
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
191
+            'upc': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'})
192
+        },
193
+        'catalogue.productattribute': {
194
+            'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
195
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
196
+            'entity_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntityType']", 'null': 'True', 'blank': 'True'}),
197
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
198
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
199
+            'option_group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOptionGroup']", 'null': 'True', 'blank': 'True'}),
200
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attributes'", 'null': 'True', 'to': "orm['catalogue.ProductClass']"}),
201
+            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
202
+            'type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '20'})
203
+        },
204
+        'catalogue.productattributevalue': {
205
+            'Meta': {'object_name': 'ProductAttributeValue'},
206
+            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductAttribute']"}),
207
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
208
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"}),
209
+            'value_boolean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
210
+            'value_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
211
+            'value_entity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntity']", 'null': 'True', 'blank': 'True'}),
212
+            'value_float': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
213
+            'value_integer': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
214
+            'value_option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOption']", 'null': 'True', 'blank': 'True'}),
215
+            'value_richtext': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
216
+            'value_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
217
+        },
218
+        'catalogue.productcategory': {
219
+            'Meta': {'ordering': "['-is_canonical']", 'object_name': 'ProductCategory'},
220
+            'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Category']"}),
221
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
222
+            'is_canonical': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
223
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
224
+        },
225
+        'catalogue.productclass': {
226
+            'Meta': {'ordering': "['name']", 'object_name': 'ProductClass'},
227
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
228
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
229
+            'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
230
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'})
231
+        },
232
+        'catalogue.productrecommendation': {
233
+            'Meta': {'object_name': 'ProductRecommendation'},
234
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
235
+            'primary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'primary_recommendations'", 'to': "orm['catalogue.Product']"}),
236
+            'ranking': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
237
+            'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
238
+        },
239
+        'contenttypes.contenttype': {
240
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
241
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
242
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
243
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
244
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
245
+        },
246
+        'offer.benefit': {
247
+            'Meta': {'object_name': 'Benefit'},
248
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
249
+            'max_affected_items': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
250
+            'range': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['offer.Range']", 'null': 'True', 'blank': 'True'}),
251
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
252
+            'value': ('oscar.models.fields.PositiveDecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'})
253
+        },
254
+        'offer.condition': {
255
+            'Meta': {'object_name': 'Condition'},
256
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
257
+            'range': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['offer.Range']"}),
258
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
259
+            'value': ('oscar.models.fields.PositiveDecimalField', [], {'max_digits': '12', 'decimal_places': '2'})
260
+        },
261
+        'offer.conditionaloffer': {
262
+            'Meta': {'ordering': "['-priority']", 'object_name': 'ConditionalOffer'},
263
+            'benefit': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['offer.Benefit']"}),
264
+            'condition': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['offer.Condition']"}),
265
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
266
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
267
+            'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
268
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
269
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
270
+            'offer_type': ('django.db.models.fields.CharField', [], {'default': "'Site'", 'max_length': '128'}),
271
+            'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
272
+            'redirect_url': ('oscar.models.fields.ExtendedURLField', [], {'max_length': '200', 'blank': 'True'}),
273
+            'start_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
274
+            'total_discount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '12', 'decimal_places': '2'})
275
+        },
276
+        'offer.range': {
277
+            'Meta': {'object_name': 'Range'},
278
+            'classes': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'classes'", 'blank': 'True', 'to': "orm['catalogue.ProductClass']"}),
279
+            'excluded_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'excludes'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
280
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
281
+            'included_categories': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'includes'", 'blank': 'True', 'to': "orm['catalogue.Category']"}),
282
+            'included_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'includes'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
283
+            'includes_all_products': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
284
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'})
285
+        },
286
+        'voucher.voucher': {
287
+            'Meta': {'object_name': 'Voucher'},
288
+            'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
289
+            'date_created': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
290
+            'end_date': ('django.db.models.fields.DateField', [], {}),
291
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
292
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
293
+            'num_basket_additions': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
294
+            'num_orders': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
295
+            'offers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'vouchers'", 'symmetrical': 'False', 'to': "orm['offer.ConditionalOffer']"}),
296
+            'start_date': ('django.db.models.fields.DateField', [], {}),
297
+            'total_discount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '12', 'decimal_places': '2'}),
298
+            'usage': ('django.db.models.fields.CharField', [], {'default': "'Multi-use'", 'max_length': '128'})
299
+        }
300
+    }
301
+
302
+    complete_apps = ['basket']

oscar/apps/basket/migrations/0002_auto__add_field_line_price_incl_tax.py → oscar/apps/basket/south_migrations/0002_auto__add_field_line_price_incl_tax.py View File


oscar/apps/basket/migrations/0003_auto__add_field_line_price_excl_tax.py → oscar/apps/basket/south_migrations/0003_auto__add_field_line_price_excl_tax.py View File


oscar/apps/basket/migrations/0004_auto__add_field_line_stockrecord.py → oscar/apps/basket/south_migrations/0004_auto__add_field_line_stockrecord.py View File


oscar/apps/basket/migrations/0005_auto__add_field_line_price_currency.py → oscar/apps/basket/south_migrations/0005_auto__add_field_line_price_currency.py View File


oscar/apps/basket/migrations/0006_auto__chg_field_line_stockrecord.py → oscar/apps/basket/south_migrations/0006_auto__chg_field_line_stockrecord.py View File


sites/us/apps/checkout/migrations/__init__.py → oscar/apps/basket/south_migrations/__init__.py View File


+ 34
- 38
oscar/apps/basket/views.py View File

@@ -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')

+ 1
- 0
oscar/apps/catalogue/__init__.py View File

@@ -0,0 +1 @@
1
+default_app_config = 'oscar.apps.catalogue.config.CatalogueConfig'

+ 172
- 54
oscar/apps/catalogue/abstract_models.py View File

@@ -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.
@@ -61,14 +63,20 @@ class AbstractProductClass(models.Model):
61 63
 
62 64
     class Meta:
63 65
         abstract = True
66
+        app_label = 'catalogue'
64 67
         ordering = ['name']
65
-        verbose_name = _("Product Class")
66
-        verbose_name_plural = _("Product Classes")
68
+        verbose_name = _("Product class")
69
+        verbose_name_plural = _("Product classes")
67 70
 
68
-    def __unicode__(self):
71
+    def __str__(self):
69 72
         return self.name
70 73
 
74
+    @property
75
+    def has_attributes(self):
76
+        return self.attributes.exists()
77
+
71 78
 
79
+@python_2_unicode_compatible
72 80
 class AbstractCategory(MP_Node):
73 81
     """
74 82
     A product category. Merely used for navigational purposes; has no
@@ -88,7 +96,7 @@ class AbstractCategory(MP_Node):
88 96
     _slug_separator = '/'
89 97
     _full_name_separator = ' > '
90 98
 
91
-    def __unicode__(self):
99
+    def __str__(self):
92 100
         return self.full_name
93 101
 
94 102
     def update_slug(self, commit=True):
@@ -172,6 +180,7 @@ class AbstractCategory(MP_Node):
172 180
 
173 181
     class Meta:
174 182
         abstract = True
183
+        app_label = 'catalogue'
175 184
         ordering = ['full_name']
176 185
         verbose_name = _('Category')
177 186
         verbose_name_plural = _('Categories')
@@ -183,6 +192,7 @@ class AbstractCategory(MP_Node):
183 192
         return self.get_children().count()
184 193
 
185 194
 
195
+@python_2_unicode_compatible
186 196
 class AbstractProductCategory(models.Model):
187 197
     """
188 198
     Joining model between products and categories. Exists to allow customising.
@@ -193,15 +203,17 @@ class AbstractProductCategory(models.Model):
193 203
 
194 204
     class Meta:
195 205
         abstract = True
206
+        app_label = 'catalogue'
196 207
         ordering = ['product', 'category']
197
-        verbose_name = _('Product Category')
198
-        verbose_name_plural = _('Product Categories')
199 208
         unique_together = ('product', 'category')
209
+        verbose_name = _('Product category')
210
+        verbose_name_plural = _('Product categories')
200 211
 
201
-    def __unicode__(self):
212
+    def __str__(self):
202 213
         return u"<productcategory for product '%s'>" % self.product
203 214
 
204 215
 
216
+@python_2_unicode_compatible
205 217
 class AbstractProduct(models.Model):
206 218
     """
207 219
     The base product object
@@ -243,7 +255,8 @@ class AbstractProduct(models.Model):
243 255
                     " this product)."))
244 256
 
245 257
     # Title is mandatory for canonical products but optional for child products
246
-    title = models.CharField(_('Product title'), max_length=255, blank=True)
258
+    title = models.CharField(pgettext_lazy(u'Product title', u'Title'),
259
+                             max_length=255, blank=True)
247 260
     slug = models.SlugField(_('Slug'), max_length=255, unique=False)
248 261
     description = models.TextField(_('Description'), blank=True)
249 262
 
@@ -251,7 +264,7 @@ class AbstractProduct(models.Model):
251 264
     #: None for child products, they inherit their parent's product class
252 265
     product_class = models.ForeignKey(
253 266
         'catalogue.ProductClass', null=True, on_delete=models.PROTECT,
254
-        verbose_name=_('Product Type'), related_name="products",
267
+        verbose_name=_('Product type'), related_name="products",
255 268
         help_text=_("Choose what type of product this is"))
256 269
     attributes = models.ManyToManyField(
257 270
         'catalogue.ProductAttribute',
@@ -289,6 +302,8 @@ class AbstractProduct(models.Model):
289 302
     #: Determines if a product may be used in an offer. It is illegal to
290 303
     #: discount some types of product (e.g. ebooks) and this field helps
291 304
     #: merchants from avoiding discounting such products
305
+    #: Note that this flag is ignored for child products; they inherit from
306
+    #: the parent product.
292 307
     is_discountable = models.BooleanField(
293 308
         _("Is discountable?"), default=True, help_text=_(
294 309
             "This flag indicates if this product can be used in an offer "
@@ -299,6 +314,7 @@ class AbstractProduct(models.Model):
299 314
 
300 315
     class Meta:
301 316
         abstract = True
317
+        app_label = 'catalogue'
302 318
         ordering = ['-date_created']
303 319
         verbose_name = _('Product')
304 320
         verbose_name_plural = _('Products')
@@ -307,7 +323,7 @@ class AbstractProduct(models.Model):
307 323
         super(AbstractProduct, self).__init__(*args, **kwargs)
308 324
         self.attr = ProductAttributesContainer(product=self)
309 325
 
310
-    def __unicode__(self):
326
+    def __str__(self):
311 327
         if self.is_child:
312 328
             return u"%s (%s)" % (self.get_title(), self.attribute_summary)
313 329
         return self.get_title()
@@ -320,7 +336,30 @@ class AbstractProduct(models.Model):
320 336
                        kwargs={'product_slug': self.slug, 'pk': self.id})
321 337
 
322 338
     def clean(self):
323
-        # call clean method for product structure
339
+        """
340
+        Validate a product. Those are the rules:
341
+
342
+        +---------------+-------------+--------------+--------------+
343
+        |               | stand alone | parent       | child        |
344
+        +---------------+-------------+--------------+--------------+
345
+        | title         | required    | required     | optional     |
346
+        +---------------+-------------+--------------+--------------+
347
+        | product class | required    | required     | must be None |
348
+        +---------------+-------------+--------------+--------------+
349
+        | parent        | forbidden   | forbidden    | required     |
350
+        +---------------+-------------+--------------+--------------+
351
+        | stockrecords  | 0 or more   | forbidden    | required     |
352
+        +---------------+-------------+--------------+--------------+
353
+        | categories    | 1 or more   | 1 or more    | forbidden    |
354
+        +---------------+-------------+--------------+--------------+
355
+        | attributes    | optional    | optional     | optional     |
356
+        +---------------+-------------+--------------+--------------+
357
+        | rec. products | optional    | optional     | unsupported  |
358
+        +---------------+-------------+--------------+--------------+
359
+
360
+        Because the validation logic is quite complex, validation is delegated
361
+        to the sub method appropriate for the product's structure.
362
+        """
324 363
         getattr(self, '_clean_%s' % self.structure)()
325 364
         if not self.is_parent:
326 365
             self.attr.validate_attributes()
@@ -345,6 +384,9 @@ class AbstractProduct(models.Model):
345 384
         if self.parent_id and not self.parent.is_parent:
346 385
             raise ValidationError(
347 386
                 _("You can only assign child products to parent products."))
387
+        if self.product_class:
388
+            raise ValidationError(
389
+                _("A child product can't have a product class."))
348 390
 
349 391
     def _clean_parent(self):
350 392
         """
@@ -375,6 +417,23 @@ class AbstractProduct(models.Model):
375 417
     def is_child(self):
376 418
         return self.structure == self.CHILD
377 419
 
420
+    def can_be_parent(self, give_reason=False):
421
+        """
422
+        Helps decide if a the product can be turned into a parent product.
423
+        """
424
+        reason = None
425
+        if self.is_child:
426
+            reason = _('The specified parent product is a child product.')
427
+        if self.has_stockrecords:
428
+            reason = _(
429
+                "One can't add a child product to a product with stock"
430
+                " records.")
431
+        is_valid = reason is None
432
+        if give_reason:
433
+            return is_valid, reason
434
+        else:
435
+            return is_valid
436
+
378 437
     @property
379 438
     def options(self):
380 439
         pclass = self.get_product_class()
@@ -409,33 +468,33 @@ class AbstractProduct(models.Model):
409 468
         return ", ".join(pairs)
410 469
 
411 470
     @property
412
-    def min_variant_price_incl_tax(self):
471
+    def min_child_price_incl_tax(self):
413 472
         """
414
-        Return minimum variant price including tax
473
+        Return minimum child product price including tax
415 474
         """
416
-        return self._min_variant_price('price_incl_tax')
475
+        return self._min_child_price('price_incl_tax')
417 476
 
418 477
     @property
419
-    def min_variant_price_excl_tax(self):
478
+    def min_child_price_excl_tax(self):
420 479
         """
421
-        Return minimum variant price excluding tax
480
+        Return minimum child product price excluding tax
422 481
         """
423
-        return self._min_variant_price('price_excl_tax')
482
+        return self._min_child_price('price_excl_tax')
424 483
 
425
-    def _min_variant_price(self, property):
484
+    def _min_child_price(self, property):
426 485
         """
427
-        Return minimum variant price
486
+        Return minimum child product price
428 487
         """
429 488
         prices = []
430
-        for variant in self.variants.all():
431
-            if variant.has_stockrecords:
432
-                prices.append(getattr(variant.stockrecord, property))
489
+        for child in self.children.all():
490
+            if child.has_stockrecords:
491
+                prices.append(getattr(child.stockrecord, property))
433 492
         if not prices:
434 493
             return None
435 494
         prices.sort()
436 495
         return prices[0]
437 496
 
438
-    # Deprecated properties
497
+    # The properties below are based on deprecated naming conventions
439 498
 
440 499
     @property
441 500
     @deprecated
@@ -457,16 +516,33 @@ class AbstractProduct(models.Model):
457 516
     @deprecated
458 517
     def is_group(self):
459 518
         """
460
-        Test if this is a top level product and has more than 0 variants
519
+        Test if this is a parent product
461 520
         """
462 521
         return self.is_parent
463 522
 
464 523
     @property
524
+    @deprecated
465 525
     def is_variant(self):
466 526
         """Return True if a product is not a top level product"""
467 527
         return self.is_child
468 528
 
469
-    # Wrappers
529
+    @property
530
+    @deprecated
531
+    def min_variant_price_incl_tax(self):
532
+        """
533
+        Return minimum variant price including tax
534
+        """
535
+        return self._min_child_price('price_incl_tax')
536
+
537
+    @property
538
+    @deprecated
539
+    def min_variant_price_excl_tax(self):
540
+        """
541
+        Return minimum variant price excluding tax
542
+        """
543
+        return self._min_child_price('price_excl_tax')
544
+
545
+    # Wrappers for child products
470 546
 
471 547
     def get_title(self):
472 548
         """
@@ -480,15 +556,34 @@ class AbstractProduct(models.Model):
480 556
 
481 557
     def get_product_class(self):
482 558
         """
483
-        Return a product's item class
559
+        Return a product's item class. Child products inherit their parent's.
484 560
         """
485
-        if self.product_class_id or self.product_class:
486
-            return self.product_class
487
-        if self.parent and self.parent.product_class:
561
+        if self.is_child:
488 562
             return self.parent.product_class
489
-        return None
563
+        else:
564
+            return self.product_class
490 565
     get_product_class.short_description = _("Product class")
491 566
 
567
+    def get_is_discountable(self):
568
+        """
569
+        At the moment, is_discountable can't be set individually for child
570
+        products; they inherit it from their parent.
571
+        """
572
+        if self.is_child:
573
+            return self.parent.is_discountable
574
+        else:
575
+            return self.is_discountable
576
+
577
+    def get_categories(self):
578
+        """
579
+        Return a product's categories or parent's if there is a parent product.
580
+        """
581
+        if self.is_child:
582
+            return self.parent.categories
583
+        else:
584
+            return self.categories
585
+    get_categories.short_description = _("Categories")
586
+
492 587
     # Images
493 588
 
494 589
     def get_missing_image(self):
@@ -589,10 +684,11 @@ class AbstractProductRecommendation(models.Model):
589 684
 
590 685
     class Meta:
591 686
         abstract = True
592
-        verbose_name = _('Product Recommendation')
593
-        verbose_name_plural = _('Product Recomendations')
687
+        app_label = 'catalogue'
594 688
         ordering = ['primary', '-ranking']
595 689
         unique_together = ('primary', 'recommendation')
690
+        verbose_name = _('Product recommendation')
691
+        verbose_name_plural = _('Product recomendations')
596 692
 
597 693
 
598 694
 class ProductAttributesContainer(object):
@@ -614,7 +710,7 @@ class ProductAttributesContainer(object):
614 710
 
615 711
     def __getattr__(self, name):
616 712
         if not name.startswith('_') and not self.initialised:
617
-            values = list(self.get_values().select_related('attribute'))
713
+            values = self.get_values().select_related('attribute')
618 714
             for v in values:
619 715
                 setattr(self, v.attribute.code, v.value)
620 716
             self.initialised = True
@@ -661,6 +757,7 @@ class ProductAttributesContainer(object):
661 757
                 attribute.save_value(self.product, value)
662 758
 
663 759
 
760
+@python_2_unicode_compatible
664 761
 class AbstractProductAttribute(models.Model):
665 762
     """
666 763
     Defines an attribute for a product class. (For example, number_of_pages for
@@ -712,9 +809,10 @@ class AbstractProductAttribute(models.Model):
712 809
 
713 810
     class Meta:
714 811
         abstract = True
812
+        app_label = 'catalogue'
715 813
         ordering = ['code']
716
-        verbose_name = _('Product Attribute')
717
-        verbose_name_plural = _('Product Attributes')
814
+        verbose_name = _('Product attribute')
815
+        verbose_name_plural = _('Product attributes')
718 816
 
719 817
     @property
720 818
     def is_option(self):
@@ -724,7 +822,7 @@ class AbstractProductAttribute(models.Model):
724 822
     def is_file(self):
725 823
         return self.type in [self.FILE, self.IMAGE]
726 824
 
727
-    def __unicode__(self):
825
+    def __str__(self):
728 826
         return self.name
729 827
 
730 828
     def save_value(self, product, value):
@@ -815,6 +913,7 @@ class AbstractProductAttribute(models.Model):
815 913
     _validate_image = _validate_file
816 914
 
817 915
 
916
+@python_2_unicode_compatible
818 917
 class AbstractProductAttributeValue(models.Model):
819 918
     """
820 919
     The "through" model for the m2m relationship between catalogue.Product and
@@ -866,11 +965,12 @@ class AbstractProductAttributeValue(models.Model):
866 965
 
867 966
     class Meta:
868 967
         abstract = True
869
-        verbose_name = _('Product Attribute Value')
870
-        verbose_name_plural = _('Product Attribute Values')
968
+        app_label = 'catalogue'
871 969
         unique_together = ('attribute', 'product')
970
+        verbose_name = _('Product attribute value')
971
+        verbose_name_plural = _('Product attribute values')
872 972
 
873
-    def __unicode__(self):
973
+    def __str__(self):
874 974
         return self.summary()
875 975
 
876 976
     def summary(self):
@@ -900,7 +1000,7 @@ class AbstractProductAttributeValue(models.Model):
900 1000
         Returns the unicode representation of the related model. You likely
901 1001
         want to customise this (and maybe _entity_as_html) if you use entities.
902 1002
         """
903
-        return unicode(self.value)
1003
+        return six.text_type(self.value)
904 1004
 
905 1005
     @property
906 1006
     def value_as_html(self):
@@ -917,6 +1017,7 @@ class AbstractProductAttributeValue(models.Model):
917 1017
         return mark_safe(self.value)
918 1018
 
919 1019
 
1020
+@python_2_unicode_compatible
920 1021
 class AbstractAttributeOptionGroup(models.Model):
921 1022
     """
922 1023
     Defines a group of options that collectively may be used as an
@@ -926,13 +1027,14 @@ class AbstractAttributeOptionGroup(models.Model):
926 1027
     """
927 1028
     name = models.CharField(_('Name'), max_length=128)
928 1029
 
929
-    def __unicode__(self):
1030
+    def __str__(self):
930 1031
         return self.name
931 1032
 
932 1033
     class Meta:
933 1034
         abstract = True
934
-        verbose_name = _('Attribute Option Group')
935
-        verbose_name_plural = _('Attribute Option Groups')
1035
+        app_label = 'catalogue'
1036
+        verbose_name = _('Attribute option group')
1037
+        verbose_name_plural = _('Attribute option groups')
936 1038
 
937 1039
     @property
938 1040
     def option_summary(self):
@@ -940,6 +1042,7 @@ class AbstractAttributeOptionGroup(models.Model):
940 1042
         return ", ".join(options)
941 1043
 
942 1044
 
1045
+@python_2_unicode_compatible
943 1046
 class AbstractAttributeOption(models.Model):
944 1047
     """
945 1048
     Provides an option within an option group for an attribute type
@@ -950,15 +1053,17 @@ class AbstractAttributeOption(models.Model):
950 1053
         verbose_name=_("Group"))
951 1054
     option = models.CharField(_('Option'), max_length=255)
952 1055
 
953
-    def __unicode__(self):
1056
+    def __str__(self):
954 1057
         return self.option
955 1058
 
956 1059
     class Meta:
957 1060
         abstract = True
958
-        verbose_name = _('Attribute Option')
959
-        verbose_name_plural = _('Attribute Options')
1061
+        app_label = 'catalogue'
1062
+        verbose_name = _('Attribute option')
1063
+        verbose_name_plural = _('Attribute options')
960 1064
 
961 1065
 
1066
+@python_2_unicode_compatible
962 1067
 class AbstractOption(models.Model):
963 1068
     """
964 1069
     An option that can be selected for a particular item when the product
@@ -985,10 +1090,11 @@ class AbstractOption(models.Model):
985 1090
 
986 1091
     class Meta:
987 1092
         abstract = True
1093
+        app_label = 'catalogue'
988 1094
         verbose_name = _("Option")
989 1095
         verbose_name_plural = _("Options")
990 1096
 
991
-    def __unicode__(self):
1097
+    def __str__(self):
992 1098
         return self.name
993 1099
 
994 1100
     @property
@@ -1034,6 +1140,7 @@ class MissingProductImage(object):
1034 1140
                                            settings.MEDIA_ROOT))
1035 1141
 
1036 1142
 
1143
+@python_2_unicode_compatible
1037 1144
 class AbstractProductImage(models.Model):
1038 1145
     """
1039 1146
     An image of a product
@@ -1053,14 +1160,15 @@ class AbstractProductImage(models.Model):
1053 1160
 
1054 1161
     class Meta:
1055 1162
         abstract = True
1056
-        unique_together = ("product", "display_order")
1163
+        app_label = 'catalogue'
1057 1164
         # Any custom models should ensure that this ordering is unchanged, or
1058 1165
         # your query count will explode. See AbstractProduct.primary_image.
1059 1166
         ordering = ["display_order"]
1060
-        verbose_name = _('Product Image')
1061
-        verbose_name_plural = _('Product Images')
1167
+        unique_together = ("product", "display_order")
1168
+        verbose_name = _('Product image')
1169
+        verbose_name_plural = _('Product images')
1062 1170
 
1063
-    def __unicode__(self):
1171
+    def __str__(self):
1064 1172
         return u"Image of '%s'" % self.product
1065 1173
 
1066 1174
     def is_primary(self):
@@ -1068,3 +1176,13 @@ class AbstractProductImage(models.Model):
1068 1176
         Return bool if image display order is 0
1069 1177
         """
1070 1178
         return self.display_order == 0
1179
+
1180
+    def delete(self, *args, **kwargs):
1181
+        """
1182
+        Always keep the display_order as consecutive integers. This avoids
1183
+        issue #855.
1184
+        """
1185
+        super(AbstractProductImage, self).delete(*args, **kwargs)
1186
+        for idx, image in enumerate(self.product.images.all()):
1187
+            image.display_order = idx
1188
+            image.save()

+ 11
- 0
oscar/apps/catalogue/config.py View File

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

+ 0
- 4
oscar/apps/catalogue/managers.py View File

@@ -40,8 +40,6 @@ class ProductManager(models.Manager):
40 40
     def base_queryset(self):
41 41
         return self.get_queryset().base_queryset()
42 42
 
43
-    get_query_set = get_queryset  # Django 1.5 compatibility fix
44
-
45 43
 
46 44
 class BrowsableProductManager(ProductManager):
47 45
     """
@@ -52,5 +50,3 @@ class BrowsableProductManager(ProductManager):
52 50
 
53 51
     def get_queryset(self):
54 52
         return super(BrowsableProductManager, self).get_queryset().browsable()
55
-
56
-    get_query_set = get_queryset  # Django 1.5 compatibility fix

+ 291
- 403
oscar/apps/catalogue/migrations/0001_initial.py View File

@@ -1,403 +1,291 @@
1
-# encoding: utf-8
2
-import datetime
3
-from south.db import db
4
-from south.v2 import SchemaMigration
5
-from django.db import models
6
-
7
-class Migration(SchemaMigration):
8
-
9
-    def forwards(self, orm):
10
-
11
-        # Adding model 'ProductRecommendation'
12
-        db.create_table('catalogue_productrecommendation', (
13
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14
-            ('primary', self.gf('django.db.models.fields.related.ForeignKey')(related_name='primary_recommendations', to=orm['catalogue.Product'])),
15
-            ('recommendation', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Product'])),
16
-            ('ranking', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=0)),
17
-        ))
18
-        db.send_create_signal('catalogue', ['ProductRecommendation'])
19
-
20
-        # Adding model 'ProductClass'
21
-        db.create_table('catalogue_productclass', (
22
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
23
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=128)),
24
-            ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=128, db_index=True)),
25
-        ))
26
-        db.send_create_signal('catalogue', ['ProductClass'])
27
-
28
-        # Adding M2M table for field options on 'ProductClass'
29
-        db.create_table('catalogue_productclass_options', (
30
-            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
31
-            ('productclass', models.ForeignKey(orm['catalogue.productclass'], null=False)),
32
-            ('option', models.ForeignKey(orm['catalogue.option'], null=False))
33
-        ))
34
-        db.create_unique('catalogue_productclass_options', ['productclass_id', 'option_id'])
35
-
36
-        # Adding model 'Category'
37
-        db.create_table('catalogue_category', (
38
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
39
-            ('path', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
40
-            ('depth', self.gf('django.db.models.fields.PositiveIntegerField')()),
41
-            ('numchild', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
42
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
43
-            ('slug',
44
-             self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
45
-            ('full_name',
46
-             self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
47
-        ))
48
-        db.send_create_signal('catalogue', ['Category'])
49
-
50
-        # Adding model 'ProductCategory'
51
-        db.create_table('catalogue_productcategory', (
52
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
53
-            ('product', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Product'])),
54
-            ('category', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Category'])),
55
-            ('is_canonical', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True)),
56
-        ))
57
-        db.send_create_signal('catalogue', ['ProductCategory'])
58
-
59
-        # Adding model 'Product'
60
-        db.create_table('catalogue_product', (
61
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
62
-            ('upc', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=64, null=True, blank=True)),
63
-            ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='variants', null=True, to=orm['catalogue.Product'])),
64
-            ('title', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
65
-            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
66
-            ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
67
-            ('product_class', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.ProductClass'], null=True)),
68
-            ('score', self.gf('django.db.models.fields.FloatField')(default=0.0, db_index=True)),
69
-            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
70
-            ('date_updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
71
-        ))
72
-        db.send_create_signal('catalogue', ['Product'])
73
-
74
-        # Adding M2M table for field product_options on 'Product'
75
-        db.create_table('catalogue_product_product_options', (
76
-            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
77
-            ('product', models.ForeignKey(orm['catalogue.product'], null=False)),
78
-            ('option', models.ForeignKey(orm['catalogue.option'], null=False))
79
-        ))
80
-        db.create_unique('catalogue_product_product_options', ['product_id', 'option_id'])
81
-
82
-        # Adding M2M table for field related_products on 'Product'
83
-        db.create_table('catalogue_product_related_products', (
84
-            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
85
-            ('from_product', models.ForeignKey(orm['catalogue.product'], null=False)),
86
-            ('to_product', models.ForeignKey(orm['catalogue.product'], null=False))
87
-        ))
88
-        db.create_unique('catalogue_product_related_products', ['from_product_id', 'to_product_id'])
89
-
90
-        # Adding model 'ContributorRole'
91
-        db.create_table('catalogue_contributorrole', (
92
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
93
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=50)),
94
-            ('name_plural', self.gf('django.db.models.fields.CharField')(max_length=50)),
95
-            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=50, db_index=True)),
96
-        ))
97
-        db.send_create_signal('catalogue', ['ContributorRole'])
98
-
99
-        # Adding model 'Contributor'
100
-        db.create_table('catalogue_contributor', (
101
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
102
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
103
-            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
104
-        ))
105
-        db.send_create_signal('catalogue', ['Contributor'])
106
-
107
-        # Adding model 'ProductContributor'
108
-        db.create_table('catalogue_productcontributor', (
109
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
110
-            ('product', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Product'])),
111
-            ('contributor', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Contributor'])),
112
-            ('role', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.ContributorRole'])),
113
-        ))
114
-        db.send_create_signal('catalogue', ['ProductContributor'])
115
-
116
-        # Adding model 'ProductAttribute'
117
-        db.create_table('catalogue_productattribute', (
118
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
119
-            ('product_class', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='attributes', null=True, to=orm['catalogue.ProductClass'])),
120
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=128)),
121
-            ('code', self.gf('django.db.models.fields.SlugField')(max_length=128, db_index=True)),
122
-            ('type', self.gf('django.db.models.fields.CharField')(default='text', max_length=20)),
123
-            ('option_group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.AttributeOptionGroup'], null=True, blank=True)),
124
-            ('entity_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.AttributeEntityType'], null=True, blank=True)),
125
-            ('required', self.gf('django.db.models.fields.BooleanField')(default=False)),
126
-        ))
127
-        db.send_create_signal('catalogue', ['ProductAttribute'])
128
-
129
-        # Adding model 'ProductAttributeValue'
130
-        db.create_table('catalogue_productattributevalue', (
131
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
132
-            ('attribute', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.ProductAttribute'])),
133
-            ('product', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Product'])),
134
-            ('value_text', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
135
-            ('value_integer', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)),
136
-            ('value_boolean', self.gf('django.db.models.fields.BooleanField')(default=False)),
137
-            ('value_float', self.gf('django.db.models.fields.FloatField')(null=True, blank=True)),
138
-            ('value_richtext', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
139
-            ('value_date', self.gf('django.db.models.fields.DateField')(null=True, blank=True)),
140
-            ('value_option', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.AttributeOption'], null=True, blank=True)),
141
-            ('value_entity', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.AttributeEntity'], null=True, blank=True)),
142
-        ))
143
-        db.send_create_signal('catalogue', ['ProductAttributeValue'])
144
-
145
-        # Adding model 'AttributeOptionGroup'
146
-        db.create_table('catalogue_attributeoptiongroup', (
147
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
148
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=128)),
149
-        ))
150
-        db.send_create_signal('catalogue', ['AttributeOptionGroup'])
151
-
152
-        # Adding model 'AttributeOption'
153
-        db.create_table('catalogue_attributeoption', (
154
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
155
-            ('group', self.gf('django.db.models.fields.related.ForeignKey')(related_name='options', to=orm['catalogue.AttributeOptionGroup'])),
156
-            ('option', self.gf('django.db.models.fields.CharField')(max_length=255)),
157
-        ))
158
-        db.send_create_signal('catalogue', ['AttributeOption'])
159
-
160
-        # Adding model 'AttributeEntity'
161
-        db.create_table('catalogue_attributeentity', (
162
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
163
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
164
-            ('slug', self.gf('django.db.models.fields.SlugField')(db_index=True, max_length=255, blank=True)),
165
-            ('type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='entities', to=orm['catalogue.AttributeEntityType'])),
166
-        ))
167
-        db.send_create_signal('catalogue', ['AttributeEntity'])
168
-
169
-        # Adding model 'AttributeEntityType'
170
-        db.create_table('catalogue_attributeentitytype', (
171
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
172
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
173
-            ('slug', self.gf('django.db.models.fields.SlugField')(db_index=True, max_length=255, blank=True)),
174
-        ))
175
-        db.send_create_signal('catalogue', ['AttributeEntityType'])
176
-
177
-        # Adding model 'Option'
178
-        db.create_table('catalogue_option', (
179
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
180
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=128)),
181
-            ('code', self.gf('django.db.models.fields.SlugField')(max_length=128, db_index=True)),
182
-            ('type', self.gf('django.db.models.fields.CharField')(default='Required', max_length=128)),
183
-        ))
184
-        db.send_create_signal('catalogue', ['Option'])
185
-
186
-        # Adding model 'ProductImage'
187
-        db.create_table('catalogue_productimage', (
188
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
189
-            ('product', self.gf('django.db.models.fields.related.ForeignKey')(related_name='images', to=orm['catalogue.Product'])),
190
-            ('original', self.gf('django.db.models.fields.files.ImageField')(max_length=100)),
191
-            ('caption', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)),
192
-            ('display_order', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
193
-            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
194
-        ))
195
-        db.send_create_signal('catalogue', ['ProductImage'])
196
-
197
-        # Adding unique constraint on 'ProductImage', fields ['product', 'display_order']
198
-        db.create_unique('catalogue_productimage', ['product_id', 'display_order'])
199
-
200
-
201
-    def backwards(self, orm):
202
-
203
-        # Removing unique constraint on 'ProductImage', fields ['product', 'display_order']
204
-        db.delete_unique('catalogue_productimage', ['product_id', 'display_order'])
205
-
206
-        # Deleting model 'ProductRecommendation'
207
-        db.delete_table('catalogue_productrecommendation')
208
-
209
-        # Deleting model 'ProductClass'
210
-        db.delete_table('catalogue_productclass')
211
-
212
-        # Removing M2M table for field options on 'ProductClass'
213
-        db.delete_table('catalogue_productclass_options')
214
-
215
-        # Deleting model 'Category'
216
-        db.delete_table('catalogue_category')
217
-
218
-        # Deleting model 'ProductCategory'
219
-        db.delete_table('catalogue_productcategory')
220
-
221
-        # Deleting model 'Product'
222
-        db.delete_table('catalogue_product')
223
-
224
-        # Removing M2M table for field product_options on 'Product'
225
-        db.delete_table('catalogue_product_product_options')
226
-
227
-        # Removing M2M table for field related_products on 'Product'
228
-        db.delete_table('catalogue_product_related_products')
229
-
230
-        # Deleting model 'ContributorRole'
231
-        db.delete_table('catalogue_contributorrole')
232
-
233
-        # Deleting model 'Contributor'
234
-        db.delete_table('catalogue_contributor')
235
-
236
-        # Deleting model 'ProductContributor'
237
-        db.delete_table('catalogue_productcontributor')
238
-
239
-        # Deleting model 'ProductAttribute'
240
-        db.delete_table('catalogue_productattribute')
241
-
242
-        # Deleting model 'ProductAttributeValue'
243
-        db.delete_table('catalogue_productattributevalue')
244
-
245
-        # Deleting model 'AttributeOptionGroup'
246
-        db.delete_table('catalogue_attributeoptiongroup')
247
-
248
-        # Deleting model 'AttributeOption'
249
-        db.delete_table('catalogue_attributeoption')
250
-
251
-        # Deleting model 'AttributeEntity'
252
-        db.delete_table('catalogue_attributeentity')
253
-
254
-        # Deleting model 'AttributeEntityType'
255
-        db.delete_table('catalogue_attributeentitytype')
256
-
257
-        # Deleting model 'Option'
258
-        db.delete_table('catalogue_option')
259
-
260
-        # Deleting model 'ProductImage'
261
-        db.delete_table('catalogue_productimage')
262
-
263
-
264
-    models = {
265
-        'catalogue.attributeentity': {
266
-            'Meta': {'object_name': 'AttributeEntity'},
267
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
268
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
269
-            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
270
-            'type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entities'", 'to': "orm['catalogue.AttributeEntityType']"})
271
-        },
272
-        'catalogue.attributeentitytype': {
273
-            'Meta': {'object_name': 'AttributeEntityType'},
274
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
275
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
276
-            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'})
277
-        },
278
-        'catalogue.attributeoption': {
279
-            'Meta': {'object_name': 'AttributeOption'},
280
-            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'options'", 'to': "orm['catalogue.AttributeOptionGroup']"}),
281
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
282
-            'option': ('django.db.models.fields.CharField', [], {'max_length': '255'})
283
-        },
284
-        'catalogue.attributeoptiongroup': {
285
-            'Meta': {'object_name': 'AttributeOptionGroup'},
286
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
287
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
288
-        },
289
-        'catalogue.category': {
290
-            'Meta': {'ordering': "['name']", 'object_name': 'Category'},
291
-            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
292
-            'full_name': ('django.db.models.fields.CharField', [],
293
-                          {'max_length': '255', 'db_index': 'True'}),
294
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
295
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
296
-            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
297
-            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
298
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length':
299
-                                                               '255', 'db_index': 'True'})
300
-        },
301
-        'catalogue.contributor': {
302
-            'Meta': {'object_name': 'Contributor'},
303
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
304
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
305
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
306
-        },
307
-        'catalogue.contributorrole': {
308
-            'Meta': {'object_name': 'ContributorRole'},
309
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
310
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
311
-            'name_plural': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
312
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'})
313
-        },
314
-        'catalogue.option': {
315
-            'Meta': {'object_name': 'Option'},
316
-            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
317
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
318
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
319
-            'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
320
-        },
321
-        'catalogue.product': {
322
-            'Meta': {'ordering': "['-date_created']", 'object_name': 'Product'},
323
-            'attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.ProductAttribute']", 'through': "orm['catalogue.ProductAttributeValue']", 'symmetrical': 'False'}),
324
-            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Category']", 'through': "orm['catalogue.ProductCategory']", 'symmetrical': 'False'}),
325
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
326
-            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
327
-            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
328
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
329
-            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'variants'", 'null': 'True', 'to': "orm['catalogue.Product']"}),
330
-            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductClass']", 'null': 'True'}),
331
-            'product_options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
332
-            'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Product']", 'symmetrical': 'False', 'through': "orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
333
-            'related_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'relations'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
334
-            'score': ('django.db.models.fields.FloatField', [], {'default': '0.0', 'db_index': 'True'}),
335
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
336
-            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
337
-            'upc': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'})
338
-        },
339
-        'catalogue.productattribute': {
340
-            'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
341
-            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
342
-            'entity_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntityType']", 'null': 'True', 'blank': 'True'}),
343
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
344
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
345
-            'option_group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOptionGroup']", 'null': 'True', 'blank': 'True'}),
346
-            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attributes'", 'null': 'True', 'to': "orm['catalogue.ProductClass']"}),
347
-            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
348
-            'type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '20'})
349
-        },
350
-        'catalogue.productattributevalue': {
351
-            'Meta': {'object_name': 'ProductAttributeValue'},
352
-            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductAttribute']"}),
353
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
354
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"}),
355
-            'value_boolean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
356
-            'value_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
357
-            'value_entity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntity']", 'null': 'True', 'blank': 'True'}),
358
-            'value_float': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
359
-            'value_integer': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
360
-            'value_option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOption']", 'null': 'True', 'blank': 'True'}),
361
-            'value_richtext': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
362
-            'value_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
363
-        },
364
-        'catalogue.productcategory': {
365
-            'Meta': {'ordering': "['-is_canonical']", 'object_name': 'ProductCategory'},
366
-            'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Category']"}),
367
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
368
-            'is_canonical': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
369
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
370
-        },
371
-        'catalogue.productclass': {
372
-            'Meta': {'ordering': "['name']", 'object_name': 'ProductClass'},
373
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
374
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
375
-            'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
376
-            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'})
377
-        },
378
-        'catalogue.productcontributor': {
379
-            'Meta': {'object_name': 'ProductContributor'},
380
-            'contributor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Contributor']"}),
381
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
382
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"}),
383
-            'role': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ContributorRole']"})
384
-        },
385
-        'catalogue.productimage': {
386
-            'Meta': {'ordering': "['display_order']", 'unique_together': "(('product', 'display_order'),)", 'object_name': 'ProductImage'},
387
-            'caption': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
388
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
389
-            'display_order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
390
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
391
-            'original': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
392
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'images'", 'to': "orm['catalogue.Product']"})
393
-        },
394
-        'catalogue.productrecommendation': {
395
-            'Meta': {'object_name': 'ProductRecommendation'},
396
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
397
-            'primary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'primary_recommendations'", 'to': "orm['catalogue.Product']"}),
398
-            'ranking': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
399
-            'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
400
-        }
401
-    }
402
-
403
-    complete_apps = ['catalogue']
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
3
+
4
+from django.db import models, migrations
5
+import oscar.models.fields
6
+import django.core.validators
7
+import django.db.models.deletion
8
+import oscar.models.fields.autoslugfield
9
+
10
+
11
+class Migration(migrations.Migration):
12
+
13
+    dependencies = [
14
+        ('contenttypes', '0001_initial'),
15
+    ]
16
+
17
+    operations = [
18
+        migrations.CreateModel(
19
+            name='AttributeOption',
20
+            fields=[
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
+            ],
24
+            options={
25
+                'verbose_name': 'Attribute option',
26
+                'verbose_name_plural': 'Attribute options',
27
+                'abstract': False,
28
+            },
29
+            bases=(models.Model,),
30
+        ),
31
+        migrations.CreateModel(
32
+            name='AttributeOptionGroup',
33
+            fields=[
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
+            ],
37
+            options={
38
+                'verbose_name': 'Attribute option group',
39
+                'verbose_name_plural': 'Attribute option groups',
40
+                'abstract': False,
41
+            },
42
+            bases=(models.Model,),
43
+        ),
44
+        migrations.AddField(
45
+            model_name='attributeoption',
46
+            name='group',
47
+            field=models.ForeignKey(verbose_name='Group', to='catalogue.AttributeOptionGroup'),
48
+            preserve_default=True,
49
+        ),
50
+        migrations.CreateModel(
51
+            name='Category',
52
+            fields=[
53
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
54
+                ('path', models.CharField(unique=True, max_length=255)),
55
+                ('depth', models.PositiveIntegerField()),
56
+                ('numchild', models.PositiveIntegerField(default=0)),
57
+                ('name', models.CharField(verbose_name='Name', db_index=True, max_length=255)),
58
+                ('description', models.TextField(verbose_name='Description', blank=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
+            ],
63
+            options={
64
+                'verbose_name': 'Category',
65
+                'verbose_name_plural': 'Categories',
66
+                'ordering': ['full_name'],
67
+                'abstract': False,
68
+            },
69
+            bases=(models.Model,),
70
+        ),
71
+        migrations.CreateModel(
72
+            name='Option',
73
+            fields=[
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
+            ],
79
+            options={
80
+                'verbose_name': 'Option',
81
+                'verbose_name_plural': 'Options',
82
+                'abstract': False,
83
+            },
84
+            bases=(models.Model,),
85
+        ),
86
+        migrations.CreateModel(
87
+            name='Product',
88
+            fields=[
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
+                ('description', models.TextField(verbose_name='Description', 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
+            ],
102
+            options={
103
+                'verbose_name': 'Product',
104
+                'verbose_name_plural': 'Products',
105
+                'ordering': ['-date_created'],
106
+                'abstract': False,
107
+            },
108
+            bases=(models.Model,),
109
+        ),
110
+        migrations.CreateModel(
111
+            name='ProductAttribute',
112
+            fields=[
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
+            ],
120
+            options={
121
+                'verbose_name': 'Product attribute',
122
+                'verbose_name_plural': 'Product attributes',
123
+                'ordering': ['code'],
124
+                'abstract': False,
125
+            },
126
+            bases=(models.Model,),
127
+        ),
128
+        migrations.CreateModel(
129
+            name='ProductAttributeValue',
130
+            fields=[
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
+                ('value_boolean', models.NullBooleanField(verbose_name='Boolean')),
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
+                ('attribute', models.ForeignKey(verbose_name='Attribute', to='catalogue.ProductAttribute')),
142
+                ('entity_content_type', models.ForeignKey(editable=False, blank=True, to='contenttypes.ContentType', null=True)),
143
+            ],
144
+            options={
145
+                'verbose_name': 'Product attribute value',
146
+                'verbose_name_plural': 'Product attribute values',
147
+                'abstract': False,
148
+            },
149
+            bases=(models.Model,),
150
+        ),
151
+        migrations.AddField(
152
+            model_name='product',
153
+            name='attributes',
154
+            field=models.ManyToManyField(verbose_name='Attributes', through='catalogue.ProductAttributeValue', to='catalogue.ProductAttribute'),
155
+            preserve_default=True,
156
+        ),
157
+        migrations.AddField(
158
+            model_name='productattributevalue',
159
+            name='product',
160
+            field=models.ForeignKey(verbose_name='Product', to='catalogue.Product'),
161
+            preserve_default=True,
162
+        ),
163
+        migrations.AddField(
164
+            model_name='productattributevalue',
165
+            name='value_option',
166
+            field=models.ForeignKey(verbose_name='Value Option', blank=True, to='catalogue.AttributeOption', null=True),
167
+            preserve_default=True,
168
+        ),
169
+        migrations.AlterUniqueTogether(
170
+            name='productattributevalue',
171
+            unique_together=set([('attribute', 'product')]),
172
+        ),
173
+        migrations.CreateModel(
174
+            name='ProductCategory',
175
+            fields=[
176
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
177
+                ('category', models.ForeignKey(verbose_name='Category', to='catalogue.Category')),
178
+            ],
179
+            options={
180
+                'verbose_name': 'Product category',
181
+                'verbose_name_plural': 'Product categories',
182
+                'ordering': ['product', 'category'],
183
+                'abstract': False,
184
+            },
185
+            bases=(models.Model,),
186
+        ),
187
+        migrations.AddField(
188
+            model_name='product',
189
+            name='categories',
190
+            field=models.ManyToManyField(verbose_name='Categories', through='catalogue.ProductCategory', to='catalogue.Category'),
191
+            preserve_default=True,
192
+        ),
193
+        migrations.AddField(
194
+            model_name='productcategory',
195
+            name='product',
196
+            field=models.ForeignKey(verbose_name='Product', to='catalogue.Product'),
197
+            preserve_default=True,
198
+        ),
199
+        migrations.AlterUniqueTogether(
200
+            name='productcategory',
201
+            unique_together=set([('product', 'category')]),
202
+        ),
203
+        migrations.CreateModel(
204
+            name='ProductClass',
205
+            fields=[
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
+            ],
213
+            options={
214
+                'verbose_name': 'Product class',
215
+                'verbose_name_plural': 'Product classes',
216
+                'ordering': ['name'],
217
+                'abstract': False,
218
+            },
219
+            bases=(models.Model,),
220
+        ),
221
+        migrations.AddField(
222
+            model_name='productattribute',
223
+            name='product_class',
224
+            field=models.ForeignKey(verbose_name='Product Type', blank=True, to='catalogue.ProductClass', null=True),
225
+            preserve_default=True,
226
+        ),
227
+        migrations.AddField(
228
+            model_name='product',
229
+            name='product_class',
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
+            preserve_default=True,
232
+        ),
233
+        migrations.CreateModel(
234
+            name='ProductImage',
235
+            fields=[
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
+                ('product', models.ForeignKey(verbose_name='Product', to='catalogue.Product')),
242
+            ],
243
+            options={
244
+                'verbose_name': 'Product image',
245
+                'verbose_name_plural': 'Product images',
246
+                'ordering': ['display_order'],
247
+                'abstract': False,
248
+            },
249
+            bases=(models.Model,),
250
+        ),
251
+        migrations.AlterUniqueTogether(
252
+            name='productimage',
253
+            unique_together=set([('product', 'display_order')]),
254
+        ),
255
+        migrations.CreateModel(
256
+            name='ProductRecommendation',
257
+            fields=[
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
+            ],
261
+            options={
262
+                'verbose_name': 'Product recommendation',
263
+                'verbose_name_plural': 'Product recomendations',
264
+                'ordering': ['primary', '-ranking'],
265
+                'abstract': False,
266
+            },
267
+            bases=(models.Model,),
268
+        ),
269
+        migrations.AddField(
270
+            model_name='product',
271
+            name='recommended_products',
272
+            field=models.ManyToManyField(verbose_name='Recommended Products', through='catalogue.ProductRecommendation', blank=True, to='catalogue.Product'),
273
+            preserve_default=True,
274
+        ),
275
+        migrations.AddField(
276
+            model_name='productrecommendation',
277
+            name='primary',
278
+            field=models.ForeignKey(verbose_name='Primary Product', to='catalogue.Product'),
279
+            preserve_default=True,
280
+        ),
281
+        migrations.AddField(
282
+            model_name='productrecommendation',
283
+            name='recommendation',
284
+            field=models.ForeignKey(verbose_name='Recommended Product', to='catalogue.Product'),
285
+            preserve_default=True,
286
+        ),
287
+        migrations.AlterUniqueTogether(
288
+            name='productrecommendation',
289
+            unique_together=set([('primary', 'recommendation')]),
290
+        ),
291
+    ]

+ 38
- 23
oscar/apps/catalogue/models.py View File

@@ -1,51 +1,66 @@
1
+import django
2
+
1 3
 """
2 4
 Vanilla product models
3 5
 """
6
+from oscar.core.loading import is_model_registered
4 7
 from oscar.apps.catalogue.abstract_models import *  # noqa
5 8
 
6 9
 
7
-class ProductClass(AbstractProductClass):
8
-    pass
10
+if not is_model_registered('catalogue', 'ProductClass'):
11
+    class ProductClass(AbstractProductClass):
12
+        pass
9 13
 
10 14
 
11
-class Category(AbstractCategory):
12
-    pass
15
+if not is_model_registered('catalogue', 'Category'):
16
+    class Category(AbstractCategory):
17
+        pass
13 18
 
14 19
 
15
-class ProductCategory(AbstractProductCategory):
16
-    pass
20
+if not is_model_registered('catalogue', 'ProductCategory'):
21
+    class ProductCategory(AbstractProductCategory):
22
+        pass
17 23
 
18 24
 
19
-class Product(AbstractProduct):
20
-    pass
25
+if not is_model_registered('catalogue', 'Product'):
26
+    class Product(AbstractProduct):
27
+        pass
21 28
 
22 29
 
23
-class ProductRecommendation(AbstractProductRecommendation):
24
-    pass
30
+if not is_model_registered('catalogue', 'ProductRecommendation'):
31
+    class ProductRecommendation(AbstractProductRecommendation):
32
+        pass
25 33
 
26 34
 
27
-class ProductAttribute(AbstractProductAttribute):
28
-    pass
35
+if not is_model_registered('catalogue', 'ProductAttribute'):
36
+    class ProductAttribute(AbstractProductAttribute):
37
+        pass
29 38
 
30 39
 
31
-class ProductAttributeValue(AbstractProductAttributeValue):
32
-    pass
40
+if not is_model_registered('catalogue', 'ProductAttributeValue'):
41
+    class ProductAttributeValue(AbstractProductAttributeValue):
42
+        pass
33 43
 
34 44
 
35
-class AttributeOptionGroup(AbstractAttributeOptionGroup):
36
-    pass
45
+if not is_model_registered('catalogue', 'AttributeOptionGroup'):
46
+    class AttributeOptionGroup(AbstractAttributeOptionGroup):
47
+        pass
37 48
 
38 49
 
39
-class AttributeOption(AbstractAttributeOption):
40
-    pass
50
+if not is_model_registered('catalogue', 'AttributeOption'):
51
+    class AttributeOption(AbstractAttributeOption):
52
+        pass
41 53
 
42 54
 
43
-class Option(AbstractOption):
44
-    pass
55
+if not is_model_registered('catalogue', 'Option'):
56
+    class Option(AbstractOption):
57
+        pass
45 58
 
46 59
 
47
-class ProductImage(AbstractProductImage):
48
-    pass
60
+if not is_model_registered('catalogue', 'ProductImage'):
61
+    class ProductImage(AbstractProductImage):
62
+        pass
49 63
 
50 64
 
51
-from .receivers import *  # noqa
65
+if django.VERSION < (1, 7):
66
+    from . import receivers  # noqa

+ 9
- 4
oscar/apps/catalogue/reviews/abstract_models.py View File

@@ -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")),
@@ -69,6 +71,7 @@ class AbstractProductReview(models.Model):
69 71
 
70 72
     class Meta:
71 73
         abstract = True
74
+        app_label = 'reviews'
72 75
         ordering = ['-delta_votes', 'id']
73 76
         unique_together = (('product', 'user'),)
74 77
         verbose_name = _('Product review')
@@ -82,7 +85,7 @@ class AbstractProductReview(models.Model):
82 85
         }
83 86
         return reverse('catalogue:reviews-detail', kwargs=kwargs)
84 87
 
85
-    def __unicode__(self):
88
+    def __str__(self):
86 89
         return self.title
87 90
 
88 91
     def clean(self):
@@ -165,7 +168,7 @@ class AbstractProductReview(models.Model):
165 168
         review
166 169
         """
167 170
         if not user.is_authenticated():
168
-            return False, u"Only signed in users can vote"
171
+            return False, _(u"Only signed in users can vote")
169 172
         vote = self.votes.model(review=self, user=user, delta=1)
170 173
         try:
171 174
             vote.full_clean()
@@ -174,6 +177,7 @@ class AbstractProductReview(models.Model):
174 177
         return True, ""
175 178
 
176 179
 
180
+@python_2_unicode_compatible
177 181
 class AbstractVote(models.Model):
178 182
     """
179 183
     Records user ratings as yes/no vote.
@@ -193,12 +197,13 @@ class AbstractVote(models.Model):
193 197
 
194 198
     class Meta:
195 199
         abstract = True
200
+        app_label = 'reviews'
196 201
         ordering = ['-date_created']
197 202
         unique_together = (('user', 'review'),)
198 203
         verbose_name = _('Vote')
199 204
         verbose_name_plural = _('Votes')
200 205
 
201
-    def __unicode__(self):
206
+    def __str__(self):
202 207
         return u"%s vote for %s" % (self.delta, self.review)
203 208
 
204 209
     def clean(self):

+ 5
- 2
oscar/apps/catalogue/reviews/app.py View File

@@ -1,4 +1,5 @@
1 1
 from django.conf.urls import url
2
+from django.contrib.auth.decorators import login_required
2 3
 
3 4
 from oscar.core.application import Application
4 5
 from oscar.core.loading import get_class
@@ -17,8 +18,10 @@ class ProductReviewsApplication(Application):
17 18
         urls = [
18 19
             url(r'^(?P<pk>\d+)/$', self.detail_view.as_view(),
19 20
                 name='reviews-detail'),
20
-            url(r'^add/$', self.create_view.as_view(), name='reviews-add'),
21
-            url(r'^(?P<pk>\d+)/vote/$', self.vote_view.as_view(),
21
+            url(r'^add/$', self.create_view.as_view(),
22
+                name='reviews-add'),
23
+            url(r'^(?P<pk>\d+)/vote/$',
24
+                login_required(self.vote_view.as_view()),
22 25
                 name='reviews-vote'),
23 26
             url(r'^$', self.list_view.as_view(), name='reviews-list'),
24 27
         ]

+ 62
- 231
oscar/apps/catalogue/reviews/migrations/0001_initial.py View File

@@ -1,237 +1,68 @@
1 1
 # -*- coding: utf-8 -*-
2
-import datetime
3
-from south.db import db
4
-from south.v2 import SchemaMigration
5
-from django.db import models
2
+from __future__ import unicode_literals
6 3
 
7
-from oscar.core.compat import AUTH_USER_MODEL, AUTH_USER_MODEL_NAME
4
+from django.db import models, migrations
5
+import oscar.core.validators
6
+import django.db.models.deletion
7
+from django.conf import settings
8 8
 
9 9
 
10
-class Migration(SchemaMigration):
10
+class Migration(migrations.Migration):
11 11
 
12
-    def forwards(self, orm):
13
-        # Adding model 'ProductReview'
14
-        db.create_table(u'reviews_productreview', (
15
-            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
16
-            ('product', self.gf('django.db.models.fields.related.ForeignKey')(related_name='reviews', null=True, on_delete=models.SET_NULL, to=orm['catalogue.Product'])),
17
-            ('score', self.gf('django.db.models.fields.SmallIntegerField')()),
18
-            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
19
-            ('body', self.gf('django.db.models.fields.TextField')()),
20
-            ('user', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='reviews', null=True, to=orm[AUTH_USER_MODEL])),
21
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
22
-            ('email', self.gf('django.db.models.fields.EmailField')(max_length=75, null=True, blank=True)),
23
-            ('homepage', self.gf('django.db.models.fields.URLField')(max_length=200, null=True, blank=True)),
24
-            ('status', self.gf('django.db.models.fields.SmallIntegerField')(default=1)),
25
-            ('total_votes', self.gf('django.db.models.fields.IntegerField')(default=0)),
26
-            ('delta_votes', self.gf('django.db.models.fields.IntegerField')(default=0, db_index=True)),
27
-            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
28
-        ))
29
-        db.send_create_signal(u'reviews', ['ProductReview'])
12
+    dependencies = [
13
+        ('catalogue', '0001_initial'),
14
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15
+    ]
30 16
 
31
-        # Adding unique constraint on 'ProductReview', fields ['product', 'user']
32
-        db.create_unique(u'reviews_productreview', ['product_id', 'user_id'])
33
-
34
-        # Adding model 'Vote'
35
-        db.create_table(u'reviews_vote', (
36
-            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
37
-            ('review', self.gf('django.db.models.fields.related.ForeignKey')(related_name='votes', to=orm['reviews.ProductReview'])),
38
-            ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='review_votes', to=orm[AUTH_USER_MODEL])),
39
-            ('delta', self.gf('django.db.models.fields.SmallIntegerField')()),
40
-            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
41
-        ))
42
-        db.send_create_signal(u'reviews', ['Vote'])
43
-
44
-        # Adding unique constraint on 'Vote', fields ['user', 'review']
45
-        db.create_unique(u'reviews_vote', ['user_id', 'review_id'])
46
-
47
-
48
-    def backwards(self, orm):
49
-        # Removing unique constraint on 'Vote', fields ['user', 'review']
50
-        db.delete_unique(u'reviews_vote', ['user_id', 'review_id'])
51
-
52
-        # Removing unique constraint on 'ProductReview', fields ['product', 'user']
53
-        db.delete_unique(u'reviews_productreview', ['product_id', 'user_id'])
54
-
55
-        # Deleting model 'ProductReview'
56
-        db.delete_table(u'reviews_productreview')
57
-
58
-        # Deleting model 'Vote'
59
-        db.delete_table(u'reviews_vote')
60
-
61
-
62
-    models = {
63
-        u'auth.group': {
64
-            'Meta': {'object_name': 'Group'},
65
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
66
-            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
67
-            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
68
-        },
69
-        u'auth.permission': {
70
-            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
71
-            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
72
-            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
73
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
74
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
75
-        },
76
-        AUTH_USER_MODEL: {
77
-            'Meta': {'object_name': AUTH_USER_MODEL_NAME},
78
-            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
79
-            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
80
-            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
81
-            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
82
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
83
-            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
84
-            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
85
-            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
86
-            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
87
-            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
88
-            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
89
-            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
90
-            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
91
-        },
92
-        u'catalogue.attributeentity': {
93
-            'Meta': {'object_name': 'AttributeEntity'},
94
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
95
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
96
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'blank': 'True'}),
97
-            'type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entities'", 'to': u"orm['catalogue.AttributeEntityType']"})
98
-        },
99
-        u'catalogue.attributeentitytype': {
100
-            'Meta': {'object_name': 'AttributeEntityType'},
101
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
102
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
103
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'blank': 'True'})
104
-        },
105
-        u'catalogue.attributeoption': {
106
-            'Meta': {'object_name': 'AttributeOption'},
107
-            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'options'", 'to': u"orm['catalogue.AttributeOptionGroup']"}),
108
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
109
-            'option': ('django.db.models.fields.CharField', [], {'max_length': '255'})
110
-        },
111
-        u'catalogue.attributeoptiongroup': {
112
-            'Meta': {'object_name': 'AttributeOptionGroup'},
113
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
114
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
115
-        },
116
-        u'catalogue.category': {
117
-            'Meta': {'ordering': "['full_name']", 'object_name': 'Category'},
118
-            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
119
-            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
120
-            'full_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
121
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
122
-            'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
123
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
124
-            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
125
-            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
126
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'})
127
-        },
128
-        u'catalogue.option': {
129
-            'Meta': {'object_name': 'Option'},
130
-            'code': ('oscar.models.fields.autoslugfield.AutoSlugField', [], {'allow_duplicates': 'False', 'max_length': '128', 'separator': "u'-'", 'blank': 'True', 'unique': 'True', 'populate_from': "'name'", 'overwrite': 'False'}),
131
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
132
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
133
-            'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
134
-        },
135
-        u'catalogue.product': {
136
-            'Meta': {'ordering': "['-date_created']", 'object_name': 'Product'},
137
-            'attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.ProductAttribute']", 'through': u"orm['catalogue.ProductAttributeValue']", 'symmetrical': 'False'}),
138
-            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Category']", 'through': u"orm['catalogue.ProductCategory']", 'symmetrical': 'False'}),
139
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
140
-            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
141
-            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
142
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
143
-            'is_discountable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
144
-            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'variants'", 'null': 'True', 'to': u"orm['catalogue.Product']"}),
145
-            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'products'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': u"orm['catalogue.ProductClass']"}),
146
-            'product_options': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
147
-            'rating': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
148
-            'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Product']", 'symmetrical': 'False', 'through': u"orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
149
-            'related_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'relations'", 'blank': 'True', 'to': u"orm['catalogue.Product']"}),
150
-            'score': ('django.db.models.fields.FloatField', [], {'default': '0.0', 'db_index': 'True'}),
151
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
152
-            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
153
-            'upc': ('oscar.models.fields.NullCharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
154
-        },
155
-        u'catalogue.productattribute': {
156
-            'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
157
-            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128'}),
158
-            'entity_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.AttributeEntityType']", 'null': 'True', 'blank': 'True'}),
159
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
160
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
161
-            'option_group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.AttributeOptionGroup']", 'null': 'True', 'blank': 'True'}),
162
-            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attributes'", 'null': 'True', 'to': u"orm['catalogue.ProductClass']"}),
163
-            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
164
-            'type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '20'})
165
-        },
166
-        u'catalogue.productattributevalue': {
167
-            'Meta': {'object_name': 'ProductAttributeValue'},
168
-            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.ProductAttribute']"}),
169
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
170
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_values'", 'to': u"orm['catalogue.Product']"}),
171
-            'value_boolean': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
172
-            'value_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
173
-            'value_entity': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.AttributeEntity']", 'null': 'True', 'blank': 'True'}),
174
-            'value_file': ('django.db.models.fields.files.FileField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
175
-            'value_float': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
176
-            'value_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
177
-            'value_integer': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
178
-            'value_option': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.AttributeOption']", 'null': 'True', 'blank': 'True'}),
179
-            'value_richtext': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
180
-            'value_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
181
-        },
182
-        u'catalogue.productcategory': {
183
-            'Meta': {'ordering': "['product', 'category']", 'object_name': 'ProductCategory'},
184
-            'category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Category']"}),
185
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
186
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Product']"})
187
-        },
188
-        u'catalogue.productclass': {
189
-            'Meta': {'ordering': "['name']", 'object_name': 'ProductClass'},
190
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
191
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
192
-            'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
193
-            'requires_shipping': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
194
-            'slug': ('oscar.models.fields.autoslugfield.AutoSlugField', [], {'allow_duplicates': 'False', 'max_length': '128', 'separator': "u'-'", 'blank': 'True', 'unique': 'True', 'populate_from': "'name'", 'overwrite': 'False'}),
195
-            'track_stock': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
196
-        },
197
-        u'catalogue.productrecommendation': {
198
-            'Meta': {'object_name': 'ProductRecommendation'},
199
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
200
-            'primary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'primary_recommendations'", 'to': u"orm['catalogue.Product']"}),
201
-            'ranking': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
202
-            'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Product']"})
203
-        },
204
-        u'contenttypes.contenttype': {
205
-            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
206
-            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
207
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
208
-            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
209
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
210
-        },
211
-        u'reviews.productreview': {
212
-            'Meta': {'ordering': "['-delta_votes', 'id']", 'unique_together': "(('product', 'user'),)", 'object_name': 'ProductReview'},
213
-            'body': ('django.db.models.fields.TextField', [], {}),
214
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
215
-            'delta_votes': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}),
216
-            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
217
-            'homepage': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
218
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
219
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
220
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reviews'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['catalogue.Product']"}),
221
-            'score': ('django.db.models.fields.SmallIntegerField', [], {}),
222
-            'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
223
-            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
224
-            'total_votes': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
225
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'reviews'", 'null': 'True', 'to': u"orm['{0}']".format(AUTH_USER_MODEL)})
226
-        },
227
-        u'reviews.vote': {
228
-            'Meta': {'ordering': "['-date_created']", 'unique_together': "(('user', 'review'),)", 'object_name': 'Vote'},
229
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
230
-            'delta': ('django.db.models.fields.SmallIntegerField', [], {}),
231
-            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
232
-            'review': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': u"orm['reviews.ProductReview']"}),
233
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'review_votes'", 'to': u"orm['{0}']".format(AUTH_USER_MODEL)})
234
-        }
235
-    }
236
-
237
-    complete_apps = ['reviews']
17
+    operations = [
18
+        migrations.CreateModel(
19
+            name='ProductReview',
20
+            fields=[
21
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
22
+                ('score', models.SmallIntegerField(verbose_name='Score', choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)])),
23
+                ('title', models.CharField(verbose_name='Title', validators=[oscar.core.validators.non_whitespace], max_length=255)),
24
+                ('body', models.TextField(verbose_name='Body')),
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
+                ('homepage', models.URLField(verbose_name='URL', blank=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
+                ('date_created', models.DateTimeField(auto_now_add=True)),
32
+                ('product', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, to='catalogue.Product', null=True)),
33
+                ('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True)),
34
+            ],
35
+            options={
36
+                'verbose_name': 'Product review',
37
+                'verbose_name_plural': 'Product reviews',
38
+                'ordering': ['-delta_votes', 'id'],
39
+                'abstract': False,
40
+            },
41
+            bases=(models.Model,),
42
+        ),
43
+        migrations.AlterUniqueTogether(
44
+            name='productreview',
45
+            unique_together=set([('product', 'user')]),
46
+        ),
47
+        migrations.CreateModel(
48
+            name='Vote',
49
+            fields=[
50
+                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
51
+                ('delta', models.SmallIntegerField(verbose_name='Delta', choices=[(1, 'Up'), (-1, 'Down')])),
52
+                ('date_created', models.DateTimeField(auto_now_add=True)),
53
+                ('review', models.ForeignKey(to='reviews.ProductReview')),
54
+                ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
55
+            ],
56
+            options={
57
+                'verbose_name': 'Vote',
58
+                'verbose_name_plural': 'Votes',
59
+                'ordering': ['-date_created'],
60
+                'abstract': False,
61
+            },
62
+            bases=(models.Model,),
63
+        ),
64
+        migrations.AlterUniqueTogether(
65
+            name='vote',
66
+            unique_together=set([('user', 'review')]),
67
+        ),
68
+    ]

+ 7
- 4
oscar/apps/catalogue/reviews/models.py View File

@@ -1,10 +1,13 @@
1
+from oscar.core.loading import is_model_registered
1 2
 from oscar.apps.catalogue.reviews.abstract_models import \
2 3
     AbstractProductReview, AbstractVote
3 4
 
4 5
 
5
-class ProductReview(AbstractProductReview):
6
-    pass
6
+if not is_model_registered('reviews', 'ProductReview'):
7
+    class ProductReview(AbstractProductReview):
8
+        pass
7 9
 
8 10
 
9
-class Vote(AbstractVote):
10
-    pass
11
+if not is_model_registered('reviews', 'Vote'):
12
+    class Vote(AbstractVote):
13
+        pass

+ 237
- 0
oscar/apps/catalogue/reviews/south_migrations/0001_initial.py View File

@@ -0,0 +1,237 @@
1
+# -*- coding: utf-8 -*-
2
+import datetime
3
+from south.db import db
4
+from south.v2 import SchemaMigration
5
+from django.db import models
6
+
7
+from oscar.core.compat import AUTH_USER_MODEL, AUTH_USER_MODEL_NAME
8
+
9
+
10
+class Migration(SchemaMigration):
11
+
12
+    def forwards(self, orm):
13
+        # Adding model 'ProductReview'
14
+        db.create_table(u'reviews_productreview', (
15
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
16
+            ('product', self.gf('django.db.models.fields.related.ForeignKey')(related_name='reviews', null=True, on_delete=models.SET_NULL, to=orm['catalogue.Product'])),
17
+            ('score', self.gf('django.db.models.fields.SmallIntegerField')()),
18
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
19
+            ('body', self.gf('django.db.models.fields.TextField')()),
20
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='reviews', null=True, to=orm[AUTH_USER_MODEL])),
21
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
22
+            ('email', self.gf('django.db.models.fields.EmailField')(max_length=75, null=True, blank=True)),
23
+            ('homepage', self.gf('django.db.models.fields.URLField')(max_length=200, null=True, blank=True)),
24
+            ('status', self.gf('django.db.models.fields.SmallIntegerField')(default=1)),
25
+            ('total_votes', self.gf('django.db.models.fields.IntegerField')(default=0)),
26
+            ('delta_votes', self.gf('django.db.models.fields.IntegerField')(default=0, db_index=True)),
27
+            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
28
+        ))
29
+        db.send_create_signal(u'reviews', ['ProductReview'])
30
+
31
+        # Adding unique constraint on 'ProductReview', fields ['product', 'user']
32
+        db.create_unique(u'reviews_productreview', ['product_id', 'user_id'])
33
+
34
+        # Adding model 'Vote'
35
+        db.create_table(u'reviews_vote', (
36
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
37
+            ('review', self.gf('django.db.models.fields.related.ForeignKey')(related_name='votes', to=orm['reviews.ProductReview'])),
38
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='review_votes', to=orm[AUTH_USER_MODEL])),
39
+            ('delta', self.gf('django.db.models.fields.SmallIntegerField')()),
40
+            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
41
+        ))
42
+        db.send_create_signal(u'reviews', ['Vote'])
43
+
44
+        # Adding unique constraint on 'Vote', fields ['user', 'review']
45
+        db.create_unique(u'reviews_vote', ['user_id', 'review_id'])
46
+
47
+
48
+    def backwards(self, orm):
49
+        # Removing unique constraint on 'Vote', fields ['user', 'review']
50
+        db.delete_unique(u'reviews_vote', ['user_id', 'review_id'])
51
+
52
+        # Removing unique constraint on 'ProductReview', fields ['product', 'user']
53
+        db.delete_unique(u'reviews_productreview', ['product_id', 'user_id'])
54
+
55
+        # Deleting model 'ProductReview'
56
+        db.delete_table(u'reviews_productreview')
57
+
58
+        # Deleting model 'Vote'
59
+        db.delete_table(u'reviews_vote')
60
+
61
+
62
+    models = {
63
+        u'auth.group': {
64
+            'Meta': {'object_name': 'Group'},
65
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
66
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
67
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
68
+        },
69
+        u'auth.permission': {
70
+            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
71
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
72
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
73
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
74
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
75
+        },
76
+        AUTH_USER_MODEL: {
77
+            'Meta': {'object_name': AUTH_USER_MODEL_NAME},
78
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
79
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
80
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
81
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
82
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
83
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
84
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
85
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
86
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
87
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
88
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
89
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
90
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
91
+        },
92
+        u'catalogue.attributeentity': {
93
+            'Meta': {'object_name': 'AttributeEntity'},
94
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
95
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
96
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'blank': 'True'}),
97
+            'type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entities'", 'to': u"orm['catalogue.AttributeEntityType']"})
98
+        },
99
+        u'catalogue.attributeentitytype': {
100
+            'Meta': {'object_name': 'AttributeEntityType'},
101
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
102
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
103
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'blank': 'True'})
104
+        },
105
+        u'catalogue.attributeoption': {
106
+            'Meta': {'object_name': 'AttributeOption'},
107
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'options'", 'to': u"orm['catalogue.AttributeOptionGroup']"}),
108
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
109
+            'option': ('django.db.models.fields.CharField', [], {'max_length': '255'})
110
+        },
111
+        u'catalogue.attributeoptiongroup': {
112
+            'Meta': {'object_name': 'AttributeOptionGroup'},
113
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
114
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
115
+        },
116
+        u'catalogue.category': {
117
+            'Meta': {'ordering': "['full_name']", 'object_name': 'Category'},
118
+            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
119
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
120
+            'full_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
121
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
122
+            'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
123
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
124
+            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
125
+            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
126
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'})
127
+        },
128
+        u'catalogue.option': {
129
+            'Meta': {'object_name': 'Option'},
130
+            'code': ('oscar.models.fields.autoslugfield.AutoSlugField', [], {'allow_duplicates': 'False', 'max_length': '128', 'separator': "u'-'", 'blank': 'True', 'unique': 'True', 'populate_from': "'name'", 'overwrite': 'False'}),
131
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
132
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
133
+            'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
134
+        },
135
+        u'catalogue.product': {
136
+            'Meta': {'ordering': "['-date_created']", 'object_name': 'Product'},
137
+            'attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.ProductAttribute']", 'through': u"orm['catalogue.ProductAttributeValue']", 'symmetrical': 'False'}),
138
+            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Category']", 'through': u"orm['catalogue.ProductCategory']", 'symmetrical': 'False'}),
139
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
140
+            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
141
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
142
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
143
+            'is_discountable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
144
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'variants'", 'null': 'True', 'to': u"orm['catalogue.Product']"}),
145
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'products'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': u"orm['catalogue.ProductClass']"}),
146
+            'product_options': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
147
+            'rating': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
148
+            'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Product']", 'symmetrical': 'False', 'through': u"orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
149
+            'related_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'relations'", 'blank': 'True', 'to': u"orm['catalogue.Product']"}),
150
+            'score': ('django.db.models.fields.FloatField', [], {'default': '0.0', 'db_index': 'True'}),
151
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
152
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
153
+            'upc': ('oscar.models.fields.NullCharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
154
+        },
155
+        u'catalogue.productattribute': {
156
+            'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
157
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128'}),
158
+            'entity_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.AttributeEntityType']", 'null': 'True', 'blank': 'True'}),
159
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
160
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
161
+            'option_group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.AttributeOptionGroup']", 'null': 'True', 'blank': 'True'}),
162
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attributes'", 'null': 'True', 'to': u"orm['catalogue.ProductClass']"}),
163
+            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
164
+            'type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '20'})
165
+        },
166
+        u'catalogue.productattributevalue': {
167
+            'Meta': {'object_name': 'ProductAttributeValue'},
168
+            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.ProductAttribute']"}),
169
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
170
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_values'", 'to': u"orm['catalogue.Product']"}),
171
+            'value_boolean': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
172
+            'value_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
173
+            'value_entity': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.AttributeEntity']", 'null': 'True', 'blank': 'True'}),
174
+            'value_file': ('django.db.models.fields.files.FileField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
175
+            'value_float': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
176
+            'value_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
177
+            'value_integer': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
178
+            'value_option': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.AttributeOption']", 'null': 'True', 'blank': 'True'}),
179
+            'value_richtext': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
180
+            'value_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
181
+        },
182
+        u'catalogue.productcategory': {
183
+            'Meta': {'ordering': "['product', 'category']", 'object_name': 'ProductCategory'},
184
+            'category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Category']"}),
185
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
186
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Product']"})
187
+        },
188
+        u'catalogue.productclass': {
189
+            'Meta': {'ordering': "['name']", 'object_name': 'ProductClass'},
190
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
191
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
192
+            'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
193
+            'requires_shipping': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
194
+            'slug': ('oscar.models.fields.autoslugfield.AutoSlugField', [], {'allow_duplicates': 'False', 'max_length': '128', 'separator': "u'-'", 'blank': 'True', 'unique': 'True', 'populate_from': "'name'", 'overwrite': 'False'}),
195
+            'track_stock': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
196
+        },
197
+        u'catalogue.productrecommendation': {
198
+            'Meta': {'object_name': 'ProductRecommendation'},
199
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
200
+            'primary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'primary_recommendations'", 'to': u"orm['catalogue.Product']"}),
201
+            'ranking': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
202
+            'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Product']"})
203
+        },
204
+        u'contenttypes.contenttype': {
205
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
206
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
207
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
208
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
209
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
210
+        },
211
+        u'reviews.productreview': {
212
+            'Meta': {'ordering': "['-delta_votes', 'id']", 'unique_together': "(('product', 'user'),)", 'object_name': 'ProductReview'},
213
+            'body': ('django.db.models.fields.TextField', [], {}),
214
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
215
+            'delta_votes': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}),
216
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
217
+            'homepage': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
218
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
219
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
220
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reviews'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['catalogue.Product']"}),
221
+            'score': ('django.db.models.fields.SmallIntegerField', [], {}),
222
+            'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
223
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
224
+            'total_votes': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
225
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'reviews'", 'null': 'True', 'to': u"orm['{0}']".format(AUTH_USER_MODEL)})
226
+        },
227
+        u'reviews.vote': {
228
+            'Meta': {'ordering': "['-date_created']", 'unique_together': "(('user', 'review'),)", 'object_name': 'Vote'},
229
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
230
+            'delta': ('django.db.models.fields.SmallIntegerField', [], {}),
231
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
232
+            'review': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': u"orm['reviews.ProductReview']"}),
233
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'review_votes'", 'to': u"orm['{0}']".format(AUTH_USER_MODEL)})
234
+        }
235
+    }
236
+
237
+    complete_apps = ['reviews']

oscar/apps/catalogue/reviews/migrations/0002_no_null_in_charfields.py → oscar/apps/catalogue/reviews/south_migrations/0002_no_null_in_charfields.py View File


oscar/apps/catalogue/reviews/migrations/0003_auto__chg_field_productreview_name__chg_field_productreview_homepage__.py → oscar/apps/catalogue/reviews/south_migrations/0003_auto__chg_field_productreview_name__chg_field_productreview_homepage__.py View File


sites/us/apps/partner/migrations/__init__.py → oscar/apps/catalogue/reviews/south_migrations/__init__.py View File


+ 4
- 5
oscar/apps/catalogue/reviews/views.py View File

@@ -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):

+ 403
- 0
oscar/apps/catalogue/south_migrations/0001_initial.py View File

@@ -0,0 +1,403 @@
1
+# encoding: utf-8
2
+import datetime
3
+from south.db import db
4
+from south.v2 import SchemaMigration
5
+from django.db import models
6
+
7
+class Migration(SchemaMigration):
8
+
9
+    def forwards(self, orm):
10
+
11
+        # Adding model 'ProductRecommendation'
12
+        db.create_table('catalogue_productrecommendation', (
13
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14
+            ('primary', self.gf('django.db.models.fields.related.ForeignKey')(related_name='primary_recommendations', to=orm['catalogue.Product'])),
15
+            ('recommendation', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Product'])),
16
+            ('ranking', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=0)),
17
+        ))
18
+        db.send_create_signal('catalogue', ['ProductRecommendation'])
19
+
20
+        # Adding model 'ProductClass'
21
+        db.create_table('catalogue_productclass', (
22
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
23
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=128)),
24
+            ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=128, db_index=True)),
25
+        ))
26
+        db.send_create_signal('catalogue', ['ProductClass'])
27
+
28
+        # Adding M2M table for field options on 'ProductClass'
29
+        db.create_table('catalogue_productclass_options', (
30
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
31
+            ('productclass', models.ForeignKey(orm['catalogue.productclass'], null=False)),
32
+            ('option', models.ForeignKey(orm['catalogue.option'], null=False))
33
+        ))
34
+        db.create_unique('catalogue_productclass_options', ['productclass_id', 'option_id'])
35
+
36
+        # Adding model 'Category'
37
+        db.create_table('catalogue_category', (
38
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
39
+            ('path', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
40
+            ('depth', self.gf('django.db.models.fields.PositiveIntegerField')()),
41
+            ('numchild', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
42
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
43
+            ('slug',
44
+             self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
45
+            ('full_name',
46
+             self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
47
+        ))
48
+        db.send_create_signal('catalogue', ['Category'])
49
+
50
+        # Adding model 'ProductCategory'
51
+        db.create_table('catalogue_productcategory', (
52
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
53
+            ('product', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Product'])),
54
+            ('category', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Category'])),
55
+            ('is_canonical', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True)),
56
+        ))
57
+        db.send_create_signal('catalogue', ['ProductCategory'])
58
+
59
+        # Adding model 'Product'
60
+        db.create_table('catalogue_product', (
61
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
62
+            ('upc', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=64, null=True, blank=True)),
63
+            ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='variants', null=True, to=orm['catalogue.Product'])),
64
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
65
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
66
+            ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
67
+            ('product_class', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.ProductClass'], null=True)),
68
+            ('score', self.gf('django.db.models.fields.FloatField')(default=0.0, db_index=True)),
69
+            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
70
+            ('date_updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
71
+        ))
72
+        db.send_create_signal('catalogue', ['Product'])
73
+
74
+        # Adding M2M table for field product_options on 'Product'
75
+        db.create_table('catalogue_product_product_options', (
76
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
77
+            ('product', models.ForeignKey(orm['catalogue.product'], null=False)),
78
+            ('option', models.ForeignKey(orm['catalogue.option'], null=False))
79
+        ))
80
+        db.create_unique('catalogue_product_product_options', ['product_id', 'option_id'])
81
+
82
+        # Adding M2M table for field related_products on 'Product'
83
+        db.create_table('catalogue_product_related_products', (
84
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
85
+            ('from_product', models.ForeignKey(orm['catalogue.product'], null=False)),
86
+            ('to_product', models.ForeignKey(orm['catalogue.product'], null=False))
87
+        ))
88
+        db.create_unique('catalogue_product_related_products', ['from_product_id', 'to_product_id'])
89
+
90
+        # Adding model 'ContributorRole'
91
+        db.create_table('catalogue_contributorrole', (
92
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
93
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=50)),
94
+            ('name_plural', self.gf('django.db.models.fields.CharField')(max_length=50)),
95
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=50, db_index=True)),
96
+        ))
97
+        db.send_create_signal('catalogue', ['ContributorRole'])
98
+
99
+        # Adding model 'Contributor'
100
+        db.create_table('catalogue_contributor', (
101
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
102
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
103
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
104
+        ))
105
+        db.send_create_signal('catalogue', ['Contributor'])
106
+
107
+        # Adding model 'ProductContributor'
108
+        db.create_table('catalogue_productcontributor', (
109
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
110
+            ('product', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Product'])),
111
+            ('contributor', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Contributor'])),
112
+            ('role', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.ContributorRole'])),
113
+        ))
114
+        db.send_create_signal('catalogue', ['ProductContributor'])
115
+
116
+        # Adding model 'ProductAttribute'
117
+        db.create_table('catalogue_productattribute', (
118
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
119
+            ('product_class', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='attributes', null=True, to=orm['catalogue.ProductClass'])),
120
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=128)),
121
+            ('code', self.gf('django.db.models.fields.SlugField')(max_length=128, db_index=True)),
122
+            ('type', self.gf('django.db.models.fields.CharField')(default='text', max_length=20)),
123
+            ('option_group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.AttributeOptionGroup'], null=True, blank=True)),
124
+            ('entity_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.AttributeEntityType'], null=True, blank=True)),
125
+            ('required', self.gf('django.db.models.fields.BooleanField')(default=False)),
126
+        ))
127
+        db.send_create_signal('catalogue', ['ProductAttribute'])
128
+
129
+        # Adding model 'ProductAttributeValue'
130
+        db.create_table('catalogue_productattributevalue', (
131
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
132
+            ('attribute', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.ProductAttribute'])),
133
+            ('product', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Product'])),
134
+            ('value_text', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
135
+            ('value_integer', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)),
136
+            ('value_boolean', self.gf('django.db.models.fields.BooleanField')(default=False)),
137
+            ('value_float', self.gf('django.db.models.fields.FloatField')(null=True, blank=True)),
138
+            ('value_richtext', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
139
+            ('value_date', self.gf('django.db.models.fields.DateField')(null=True, blank=True)),
140
+            ('value_option', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.AttributeOption'], null=True, blank=True)),
141
+            ('value_entity', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.AttributeEntity'], null=True, blank=True)),
142
+        ))
143
+        db.send_create_signal('catalogue', ['ProductAttributeValue'])
144
+
145
+        # Adding model 'AttributeOptionGroup'
146
+        db.create_table('catalogue_attributeoptiongroup', (
147
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
148
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=128)),
149
+        ))
150
+        db.send_create_signal('catalogue', ['AttributeOptionGroup'])
151
+
152
+        # Adding model 'AttributeOption'
153
+        db.create_table('catalogue_attributeoption', (
154
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
155
+            ('group', self.gf('django.db.models.fields.related.ForeignKey')(related_name='options', to=orm['catalogue.AttributeOptionGroup'])),
156
+            ('option', self.gf('django.db.models.fields.CharField')(max_length=255)),
157
+        ))
158
+        db.send_create_signal('catalogue', ['AttributeOption'])
159
+
160
+        # Adding model 'AttributeEntity'
161
+        db.create_table('catalogue_attributeentity', (
162
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
163
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
164
+            ('slug', self.gf('django.db.models.fields.SlugField')(db_index=True, max_length=255, blank=True)),
165
+            ('type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='entities', to=orm['catalogue.AttributeEntityType'])),
166
+        ))
167
+        db.send_create_signal('catalogue', ['AttributeEntity'])
168
+
169
+        # Adding model 'AttributeEntityType'
170
+        db.create_table('catalogue_attributeentitytype', (
171
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
172
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
173
+            ('slug', self.gf('django.db.models.fields.SlugField')(db_index=True, max_length=255, blank=True)),
174
+        ))
175
+        db.send_create_signal('catalogue', ['AttributeEntityType'])
176
+
177
+        # Adding model 'Option'
178
+        db.create_table('catalogue_option', (
179
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
180
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=128)),
181
+            ('code', self.gf('django.db.models.fields.SlugField')(max_length=128, db_index=True)),
182
+            ('type', self.gf('django.db.models.fields.CharField')(default='Required', max_length=128)),
183
+        ))
184
+        db.send_create_signal('catalogue', ['Option'])
185
+
186
+        # Adding model 'ProductImage'
187
+        db.create_table('catalogue_productimage', (
188
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
189
+            ('product', self.gf('django.db.models.fields.related.ForeignKey')(related_name='images', to=orm['catalogue.Product'])),
190
+            ('original', self.gf('django.db.models.fields.files.ImageField')(max_length=100)),
191
+            ('caption', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)),
192
+            ('display_order', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
193
+            ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
194
+        ))
195
+        db.send_create_signal('catalogue', ['ProductImage'])
196
+
197
+        # Adding unique constraint on 'ProductImage', fields ['product', 'display_order']
198
+        db.create_unique('catalogue_productimage', ['product_id', 'display_order'])
199
+
200
+
201
+    def backwards(self, orm):
202
+
203
+        # Removing unique constraint on 'ProductImage', fields ['product', 'display_order']
204
+        db.delete_unique('catalogue_productimage', ['product_id', 'display_order'])
205
+
206
+        # Deleting model 'ProductRecommendation'
207
+        db.delete_table('catalogue_productrecommendation')
208
+
209
+        # Deleting model 'ProductClass'
210
+        db.delete_table('catalogue_productclass')
211
+
212
+        # Removing M2M table for field options on 'ProductClass'
213
+        db.delete_table('catalogue_productclass_options')
214
+
215
+        # Deleting model 'Category'
216
+        db.delete_table('catalogue_category')
217
+
218
+        # Deleting model 'ProductCategory'
219
+        db.delete_table('catalogue_productcategory')
220
+
221
+        # Deleting model 'Product'
222
+        db.delete_table('catalogue_product')
223
+
224
+        # Removing M2M table for field product_options on 'Product'
225
+        db.delete_table('catalogue_product_product_options')
226
+
227
+        # Removing M2M table for field related_products on 'Product'
228
+        db.delete_table('catalogue_product_related_products')
229
+
230
+        # Deleting model 'ContributorRole'
231
+        db.delete_table('catalogue_contributorrole')
232
+
233
+        # Deleting model 'Contributor'
234
+        db.delete_table('catalogue_contributor')
235
+
236
+        # Deleting model 'ProductContributor'
237
+        db.delete_table('catalogue_productcontributor')
238
+
239
+        # Deleting model 'ProductAttribute'
240
+        db.delete_table('catalogue_productattribute')
241
+
242
+        # Deleting model 'ProductAttributeValue'
243
+        db.delete_table('catalogue_productattributevalue')
244
+
245
+        # Deleting model 'AttributeOptionGroup'
246
+        db.delete_table('catalogue_attributeoptiongroup')
247
+
248
+        # Deleting model 'AttributeOption'
249
+        db.delete_table('catalogue_attributeoption')
250
+
251
+        # Deleting model 'AttributeEntity'
252
+        db.delete_table('catalogue_attributeentity')
253
+
254
+        # Deleting model 'AttributeEntityType'
255
+        db.delete_table('catalogue_attributeentitytype')
256
+
257
+        # Deleting model 'Option'
258
+        db.delete_table('catalogue_option')
259
+
260
+        # Deleting model 'ProductImage'
261
+        db.delete_table('catalogue_productimage')
262
+
263
+
264
+    models = {
265
+        'catalogue.attributeentity': {
266
+            'Meta': {'object_name': 'AttributeEntity'},
267
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
268
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
269
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
270
+            'type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entities'", 'to': "orm['catalogue.AttributeEntityType']"})
271
+        },
272
+        'catalogue.attributeentitytype': {
273
+            'Meta': {'object_name': 'AttributeEntityType'},
274
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
275
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
276
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'})
277
+        },
278
+        'catalogue.attributeoption': {
279
+            'Meta': {'object_name': 'AttributeOption'},
280
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'options'", 'to': "orm['catalogue.AttributeOptionGroup']"}),
281
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
282
+            'option': ('django.db.models.fields.CharField', [], {'max_length': '255'})
283
+        },
284
+        'catalogue.attributeoptiongroup': {
285
+            'Meta': {'object_name': 'AttributeOptionGroup'},
286
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
287
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
288
+        },
289
+        'catalogue.category': {
290
+            'Meta': {'ordering': "['name']", 'object_name': 'Category'},
291
+            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
292
+            'full_name': ('django.db.models.fields.CharField', [],
293
+                          {'max_length': '255', 'db_index': 'True'}),
294
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
295
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
296
+            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
297
+            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
298
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length':
299
+                                                               '255', 'db_index': 'True'})
300
+        },
301
+        'catalogue.contributor': {
302
+            'Meta': {'object_name': 'Contributor'},
303
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
304
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
305
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
306
+        },
307
+        'catalogue.contributorrole': {
308
+            'Meta': {'object_name': 'ContributorRole'},
309
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
310
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
311
+            'name_plural': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
312
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'})
313
+        },
314
+        'catalogue.option': {
315
+            'Meta': {'object_name': 'Option'},
316
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
317
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
318
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
319
+            'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
320
+        },
321
+        'catalogue.product': {
322
+            'Meta': {'ordering': "['-date_created']", 'object_name': 'Product'},
323
+            'attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.ProductAttribute']", 'through': "orm['catalogue.ProductAttributeValue']", 'symmetrical': 'False'}),
324
+            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Category']", 'through': "orm['catalogue.ProductCategory']", 'symmetrical': 'False'}),
325
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
326
+            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
327
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
328
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
329
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'variants'", 'null': 'True', 'to': "orm['catalogue.Product']"}),
330
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductClass']", 'null': 'True'}),
331
+            'product_options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
332
+            'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Product']", 'symmetrical': 'False', 'through': "orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
333
+            'related_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'relations'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
334
+            'score': ('django.db.models.fields.FloatField', [], {'default': '0.0', 'db_index': 'True'}),
335
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
336
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
337
+            'upc': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'})
338
+        },
339
+        'catalogue.productattribute': {
340
+            'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
341
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
342
+            'entity_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntityType']", 'null': 'True', 'blank': 'True'}),
343
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
344
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
345
+            'option_group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOptionGroup']", 'null': 'True', 'blank': 'True'}),
346
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attributes'", 'null': 'True', 'to': "orm['catalogue.ProductClass']"}),
347
+            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
348
+            'type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '20'})
349
+        },
350
+        'catalogue.productattributevalue': {
351
+            'Meta': {'object_name': 'ProductAttributeValue'},
352
+            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductAttribute']"}),
353
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
354
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"}),
355
+            'value_boolean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
356
+            'value_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
357
+            'value_entity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntity']", 'null': 'True', 'blank': 'True'}),
358
+            'value_float': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
359
+            'value_integer': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
360
+            'value_option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOption']", 'null': 'True', 'blank': 'True'}),
361
+            'value_richtext': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
362
+            'value_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
363
+        },
364
+        'catalogue.productcategory': {
365
+            'Meta': {'ordering': "['-is_canonical']", 'object_name': 'ProductCategory'},
366
+            'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Category']"}),
367
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
368
+            'is_canonical': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
369
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
370
+        },
371
+        'catalogue.productclass': {
372
+            'Meta': {'ordering': "['name']", 'object_name': 'ProductClass'},
373
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
374
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
375
+            'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
376
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'})
377
+        },
378
+        'catalogue.productcontributor': {
379
+            'Meta': {'object_name': 'ProductContributor'},
380
+            'contributor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Contributor']"}),
381
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
382
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"}),
383
+            'role': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ContributorRole']"})
384
+        },
385
+        'catalogue.productimage': {
386
+            'Meta': {'ordering': "['display_order']", 'unique_together': "(('product', 'display_order'),)", 'object_name': 'ProductImage'},
387
+            'caption': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
388
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
389
+            'display_order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
390
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
391
+            'original': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
392
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'images'", 'to': "orm['catalogue.Product']"})
393
+        },
394
+        'catalogue.productrecommendation': {
395
+            'Meta': {'object_name': 'ProductRecommendation'},
396
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
397
+            'primary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'primary_recommendations'", 'to': "orm['catalogue.Product']"}),
398
+            'ranking': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
399
+            'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
400
+        }
401
+    }
402
+
403
+    complete_apps = ['catalogue']

oscar/apps/catalogue/migrations/0002_auto__add_field_product_status__add_field_category_description__add_fi.py → oscar/apps/catalogue/south_migrations/0002_auto__add_field_product_status__add_field_category_description__add_fi.py View File


oscar/apps/catalogue/migrations/0003_auto__add_unique_product_upc__chg_field_productcontributor_role.py → oscar/apps/catalogue/south_migrations/0003_auto__add_unique_product_upc__chg_field_productcontributor_role.py View File


oscar/apps/catalogue/migrations/0004_auto__chg_field_productattributevalue_value_boolean.py → oscar/apps/catalogue/south_migrations/0004_auto__chg_field_productattributevalue_value_boolean.py View File


oscar/apps/catalogue/migrations/0005_auto__chg_field_productattributevalue_value_boolean__add_field_product.py → oscar/apps/catalogue/south_migrations/0005_auto__chg_field_productattributevalue_value_boolean__add_field_product.py View File


oscar/apps/catalogue/migrations/0006_auto__add_field_product_is_discountable.py → oscar/apps/catalogue/south_migrations/0006_auto__add_field_product_is_discountable.py View File


oscar/apps/catalogue/migrations/0007_auto__add_field_productclass_requires_shipping__add_field_productclass.py → oscar/apps/catalogue/south_migrations/0007_auto__add_field_productclass_requires_shipping__add_field_productclass.py View File


oscar/apps/catalogue/migrations/0008_auto__add_unique_option_code.py → oscar/apps/catalogue/south_migrations/0008_auto__add_unique_option_code.py View File


oscar/apps/catalogue/migrations/0009_auto__add_field_product_rating.py → oscar/apps/catalogue/south_migrations/0009_auto__add_field_product_rating.py View File


oscar/apps/catalogue/migrations/0010_call_update_product_ratings.py → oscar/apps/catalogue/south_migrations/0010_call_update_product_ratings.py View File


oscar/apps/catalogue/migrations/0011_auto__chg_field_productimage_original__chg_field_category_image.py → oscar/apps/catalogue/south_migrations/0011_auto__chg_field_productimage_original__chg_field_category_image.py View File


oscar/apps/catalogue/migrations/0012_auto__chg_field_productattributevalue_value_boolean.py → oscar/apps/catalogue/south_migrations/0012_auto__chg_field_productattributevalue_value_boolean.py View File


oscar/apps/catalogue/migrations/0013_add_file_attributes.py → oscar/apps/catalogue/south_migrations/0013_add_file_attributes.py View File


oscar/apps/catalogue/migrations/0014_auto__del_field_productcategory_is_canonical.py → oscar/apps/catalogue/south_migrations/0014_auto__del_field_productcategory_is_canonical.py View File


oscar/apps/catalogue/migrations/0015_auto__chg_field_product_upc.py → oscar/apps/catalogue/south_migrations/0015_auto__chg_field_product_upc.py View File


oscar/apps/catalogue/migrations/0016_customer.py → oscar/apps/catalogue/south_migrations/0016_customer.py View File


oscar/apps/catalogue/migrations/0017_auto__del_contributor__del_productcontributor__del_contributorrole__de.py → oscar/apps/catalogue/south_migrations/0017_auto__del_contributor__del_productcontributor__del_contributorrole__de.py View File


oscar/apps/catalogue/migrations/0018_auto__chg_field_product_product_class.py → oscar/apps/catalogue/south_migrations/0018_auto__chg_field_product_product_class.py View File


oscar/apps/catalogue/migrations/0019_no_null_in_charfields.py → oscar/apps/catalogue/south_migrations/0019_no_null_in_charfields.py View File


oscar/apps/catalogue/migrations/0020_auto__chg_field_productattributevalue_value_text__chg_field_productatt.py → oscar/apps/catalogue/south_migrations/0020_auto__chg_field_productattributevalue_value_text__chg_field_productatt.py View File


oscar/apps/catalogue/migrations/0021_auto__add_unique_productattributevalue_attribute_product__add_unique_p.py → oscar/apps/catalogue/south_migrations/0021_auto__add_unique_productattributevalue_attribute_product__add_unique_p.py View File


oscar/apps/catalogue/migrations/0022_auto__del_field_product_score.py → oscar/apps/catalogue/south_migrations/0022_auto__del_field_product_score.py View File


oscar/apps/catalogue/migrations/0023_auto.py → oscar/apps/catalogue/south_migrations/0023_auto.py View File


oscar/apps/catalogue/migrations/0024_auto__del_attributeentity__del_attributeentitytype__del_field_producta.py → oscar/apps/catalogue/south_migrations/0024_auto__del_attributeentity__del_attributeentitytype__del_field_producta.py View File


oscar/apps/catalogue/migrations/0025_auto__add_field_product_structure.py → oscar/apps/catalogue/south_migrations/0025_auto__add_field_product_structure.py View File


oscar/apps/catalogue/migrations/0026_determine_product_structure.py → oscar/apps/catalogue/south_migrations/0026_determine_product_structure.py View File


sites/us/apps/shipping/migrations/__init__.py → oscar/apps/catalogue/south_migrations/__init__.py View File


+ 10
- 7
oscar/apps/catalogue/views.py View File

@@ -23,8 +23,13 @@ class ProductDetailView(DetailView):
23 23
     model = Product
24 24
     view_signal = product_viewed
25 25
     template_folder = "catalogue"
26
+
27
+    # Whether to redirect to the URL with the right path
26 28
     enforce_paths = True
27 29
 
30
+    # Whether to redirect child products to their parent's URL
31
+    enforce_parent = True
32
+
28 33
     def get(self, request, **kwargs):
29 34
         """
30 35
         Ensures that the correct URL is used before rendering a response
@@ -47,22 +52,20 @@ class ProductDetailView(DetailView):
47 52
             return super(ProductDetailView, self).get_object(queryset)
48 53
 
49 54
     def redirect_if_necessary(self, current_path, product):
50
-        if self.enforce_paths:
51
-            if product.is_child:
52
-                return HttpResponsePermanentRedirect(
53
-                    product.parent.get_absolute_url())
55
+        if self.enforce_parent and product.is_child:
56
+            return HttpResponsePermanentRedirect(
57
+                product.parent.get_absolute_url())
54 58
 
59
+        if self.enforce_paths:
55 60
             expected_path = product.get_absolute_url()
56 61
             if expected_path != urlquote(current_path):
57 62
                 return HttpResponsePermanentRedirect(expected_path)
58 63
 
59 64
     def get_context_data(self, **kwargs):
60 65
         ctx = super(ProductDetailView, self).get_context_data(**kwargs)
61
-        reviews = self.get_reviews()
62
-        ctx['reviews'] = reviews
66
+        ctx['reviews'] = self.get_reviews()
63 67
         ctx['alert_form'] = self.get_alert_form()
64 68
         ctx['has_active_alert'] = self.get_alert_status()
65
-
66 69
         return ctx
67 70
 
68 71
     def get_alert_status(self):

+ 1
- 0
oscar/apps/checkout/__init__.py View File

@@ -0,0 +1 @@
1
+default_app_config = 'oscar.apps.checkout.config.CheckoutConfig'

+ 0
- 0
oscar/apps/checkout/config.py View File


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

Loading…
Cancel
Save