瀏覽代碼

Merge branch 'releases/0.2'

Conflicts:
	oscar/__init__.py
master
David Winterbottom 13 年之前
父節點
當前提交
df1a6224d4
共有 40 個文件被更改,包括 1215 次插入329 次删除
  1. 16
    4
      README.rst
  2. 0
    16
      docs/source/components.rst
  3. 63
    9
      docs/source/contributing.rst
  4. 0
    0
      docs/source/design_decisions.rst
  5. 146
    51
      docs/source/getting_started.rst
  6. 39
    17
      docs/source/index.rst
  7. 0
    95
      docs/source/introduction_to_ecommerce.rst
  8. 167
    0
      docs/source/key_questions.rst
  9. 30
    9
      docs/source/recipes.rst
  10. 4
    3
      docs/source/recipes/how_to_configure_shipping.rst
  11. 3
    0
      docs/source/recipes/how_to_configure_stock_messaging.rst
  12. 82
    0
      docs/source/recipes/how_to_customise_templates.rst
  13. 6
    0
      docs/source/recipes/how_to_disable_an_app.rst
  14. 10
    0
      docs/source/recipes/how_to_model_your_catalogue.rst
  15. 6
    0
      docs/source/recipes/importing_a_catalogue.rst
  16. 1
    0
      docs/source/reference.rst
  17. 23
    0
      docs/source/reference/signals.rst
  18. 80
    0
      docs/source/take_a_peek.rst
  19. 3
    0
      docs/test_docs.sh
  20. 61
    32
      oscar/apps/analytics/reports.py
  21. 67
    21
      oscar/apps/basket/reports.py
  22. 15
    8
      oscar/apps/basket/tests.py
  23. 3
    3
      oscar/apps/dashboard/reports/forms.py
  24. 30
    7
      oscar/apps/dashboard/reports/reports.py
  25. 4
    4
      oscar/apps/dashboard/reports/utils.py
  26. 34
    12
      oscar/apps/dashboard/reports/views.py
  27. 42
    10
      oscar/apps/offer/reports.py
  28. 37
    12
      oscar/apps/order/reports.py
  29. 29
    11
      oscar/apps/voucher/reports.py
  30. 3
    1
      oscar/templates/catalogue/browse.html
  31. 3
    0
      oscar/templates/dashboard/catalogue/product_update.html
  32. 8
    0
      oscar/templates/dashboard/reports/index.html
  33. 24
    0
      oscar/templates/dashboard/reports/partials/offer_report.html
  34. 30
    0
      oscar/templates/dashboard/reports/partials/open_basket_report.html
  35. 32
    0
      oscar/templates/dashboard/reports/partials/order_report.html
  36. 24
    0
      oscar/templates/dashboard/reports/partials/product_report.html
  37. 30
    0
      oscar/templates/dashboard/reports/partials/submitted_basket_report.html
  38. 36
    0
      oscar/templates/dashboard/reports/partials/user_report.html
  39. 24
    0
      oscar/templates/dashboard/reports/partials/voucher_report.html
  40. 0
    4
      release.sh

+ 16
- 4
README.rst 查看文件

@@ -10,12 +10,19 @@ core functionality can be customised to suit the needs of your project.  This
10 10
 allows it to handle a wide range of e-commerce requirements, from large-scale B2C
11 11
 sites to complex B2B sites rich in domain-specific business logic.
12 12
 
13
-Oscar is used in production by:
13
+Case studies
14
+------------
14 15
 
15
-* Tata - http://www.landmarkonthenet.com
16
-* Carlsberg - Their global "We deliver more" platform is powered by Oscar.
16
+Oscar is still in active development, but is used in production by a range of
17
+companies, from large multinationals to small, boutique stores:
18
+
19
+* Tata Group - http://www.landmarkonthenet.com
20
+* Carlsberg - Their global "We Deliver More" platform is powered by Oscar (but
21
+  is a B2B site so it not browsable by the public).
17 22
 * Dolbeau - http://www.dolbeau.ca/
18
-* The UK Labour party
23
+* The UK Labour party - http://shop.labour.org.uk (will be live in early June)
24
+
25
+Many more on the way.
19 26
 
20 27
 This README is just a stub - see the following links for more details
21 28
 information:
@@ -23,6 +30,7 @@ information:
23 30
 * `Official homepage`_ 
24 31
 * `Demo site`_ (experimental) 
25 32
 * `Documentation`_ on `readthedocs.org`_
33
+* `Google Group`_ - the mailing list is django-oscar@googlegroups.com
26 34
 * `Continuous integration homepage`_ on `travis-ci.org`_
27 35
 * `Twitter account for news and updates`_
28 36
 * `Twitter account of all commits`_
@@ -37,10 +45,14 @@ information:
37 45
 .. _`travis-ci.org`: http://travis-ci.org/
38 46
 .. _`Twitter account for news and updates`: https://twitter.com/#!/django_oscar
39 47
 .. _`Twitter account of all commits`: https://twitter.com/#!/oscar_django
48
+.. _`Google Group`: https://groups.google.com/forum/?fromgroups#!forum/django-oscar
40 49
 
41 50
 Oscar was written by `David Winterbottom`_ (`@codeinthehole`_) and is developed
42 51
 and maintained by `Tangent Labs`_, a London-based digital agency.
43 52
 
53
+Oscar is released under the permissive `New BSD license`_.
54
+
44 55
 .. _`David Winterbottom`: http://codeinthehole.com
45 56
 .. _`@codeinthehole`: https://twitter.com/codeinthehole
46 57
 .. _`Tangent Labs`: http://www.tangentlabs.co.uk
58
+.. _`New BSD license`: https://github.com/tangentlabs/django-oscar/blob/master/LICENSE

+ 0
- 16
docs/source/components.rst 查看文件

@@ -1,16 +0,0 @@
1
-==========
2
-Components
3
-==========
4
-
5
-This section provides high-level descriptions of the main components of django-oscar.
6
-
7
-Contents:
8
-
9
-.. toctree::
10
-   :maxdepth: 2
11
-
12
-   components/offers
13
-   components/checkout
14
-   components/shipping
15
-   components/analytics
16
-   components/promotions

+ 63
- 9
docs/source/contributing.rst 查看文件

@@ -2,15 +2,69 @@
2 2
 Contributing
3 3
 ============
4 4
 
5
-* New features should be discussed on the mailing list (or in the meetings) before serious work starts
6
-* Pull requests will be rejected if sufficient tests aren't provided
7
-* Please update the documentation when altering behaviour or introducing new features 
5
+Some ground rules:
8 6
 
9
-Contents:
7
+* To avoid disappointment, new features should be discussed on the mailing list
8
+  (django-oscar@googlegroups.com) before serious work starts.
10 9
 
11
-.. toctree::
12
-   :maxdepth: 2
10
+* Pull requests will be rejected if sufficient tests aren't provided.
13 11
 
14
-   contributing/installation
15
-   contributing/testing
16
-   contributing/conventions
12
+* Please update the documentation when altering behaviour or introducing new features.
13
+
14
+* Follow the conventions (see below).
15
+
16
+Installation
17
+============
18
+
19
+From zero to tests passing in 2 minutes (most of which is PIL installing)::
20
+
21
+    git clone git@github.com:<username>/django-oscar.git
22
+    cd django-oscar
23
+    mkvirtualenv oscar
24
+    ./setup.py develop
25
+    pip install -r requirements.txt
26
+    ./run_tests.py
27
+
28
+Writing docs
29
+============
30
+
31
+There's a helper script for building the docs locally::
32
+
33
+    cd docs
34
+    ./test_docs.sh
35
+
36
+Conventions
37
+===========
38
+
39
+General
40
+-------
41
+
42
+* PEP8 everywhere while remaining sensible
43
+
44
+URLs
45
+----
46
+
47
+* List pages should use plurals, eg ``/products/``, ``/notifications/``
48
+
49
+* Detail pages should simply be a PK/slug on top of the list page, eg
50
+  ``/products/the-bible/``, ``/notifications/1/``
51
+  
52
+* Create pages should have 'create' as the final path segment, eg
53
+  ``/dashboard/notifications/create/``
54
+
55
+* Update pages are sometimes the same as detail pages (ie when in the
56
+  dashboard).  In those cases, just use the detail convention, eg
57
+  ``/dashboard/notifications/3/``.  If there is a distinction between the detail
58
+  page and the update page, use ``/dashboard/notifications/3/update/``.
59
+
60
+* Delete pages, eg /dashboard/notifications/3/delete/
61
+
62
+View class names
63
+----------------
64
+
65
+Classes should be named according to::
66
+
67
+    '%s%sView' % (class_name, verb)
68
+
69
+For example, ``ProductUpdateView``, ``OfferCreateView`` and
70
+``PromotionDeleteView``.  This doesn't fit all situations but it's a good basis.

docs/source/customisation.rst → docs/source/design_decisions.rst 查看文件


+ 146
- 51
docs/source/getting_started.rst 查看文件

@@ -1,76 +1,171 @@
1
-===============
2
-Getting started
1
+============================
2
+Start building your own shop
3
+============================
4
+
5
+For simplicity, let's assume you're building a new e-commerce project from
6
+scratch and have decided to use Oscar.  Let's call this shop 'frobshop'
7
+
8
+.. tip::
9
+
10
+    You can always review the set-up of the `Sandbox site`_ in case you have
11
+    trouble following the below instructions.
12
+
13
+.. _`Sandbox site`: https://github.com/tangentlabs/django-oscar/tree/releases/0.2/sandbox
14
+
15
+Install by hand
3 16
 ===============
4 17
 
5
-Install using::
18
+Install oscar (which will install Django as a dependency), then create the
19
+project::
6 20
 
7 21
     pip install django-oscar
22
+    django-admin.py startproject frobshop
23
+
24
+This will create a folder ``frobshop`` for your project.
25
+
26
+Settings
27
+--------
28
+
29
+Now edit your settings file ``frobshop.frobshop.settings.py`` to specify a
30
+database (we use sqlite for simplicity)::
31
+
32
+    DATABASES = {
33
+        'default': {
34
+            'ENGINE': 'django.db.backends.sqlite3',
35
+            'NAME': 'db.sqlite3',
36
+            'USER': '',
37
+            'PASSWORD': '',
38
+            'HOST': '',
39
+            'PORT': '',
40
+        }
41
+    }
42
+
43
+then add ``oscar.apps.basket.middleware.BasketMiddleware`` to ``MIDDLEWARE_CLASSES``, and
44
+set ``TEMPLATE_CONTEXT_PROCESSORS`` to::
45
+
46
+    TEMPLATE_CONTEXT_PROCESSORS = (
47
+        "django.contrib.auth.context_processors.auth",
48
+        "django.core.context_processors.request",
49
+        "django.core.context_processors.debug",
50
+        "django.core.context_processors.i18n",
51
+        "django.core.context_processors.media",
52
+        "django.core.context_processors.static",
53
+        "django.contrib.messages.context_processors.messages",
54
+        'oscar.apps.search.context_processors.search_form',
55
+        'oscar.apps.promotions.context_processors.promotions',
56
+        'oscar.apps.checkout.context_processors.checkout',
57
+        'oscar.core.context_processors.metadata',
58
+    ) 
59
+
60
+Next, modify ``INSTALLED_APPS`` to be a list, add ``South`` and append Oscar's core apps::
8 61
 
9
-then add::
62
+    from oscar import get_core_apps
63
+    INSTALLED_APPS = [
64
+        'django.contrib.auth',
65
+        ...
66
+        'south',
67
+    ] + get_core_apps()
10 68
 
11
-    'oscar.apps.basket.middleware.BasketMiddleware'
69
+and set your auth backends to::
12 70
 
13
-to ``MIDDLEWARE_CLASSES``, and::
71
+    AUTHENTICATION_BACKENDS = (
72
+        'oscar.apps.customer.auth_backends.Emailbackend',
73
+        'django.contrib.auth.backends.ModelBackend',
74
+    )
14 75
 
15
-    'oscar.apps.promotions.context_processors.promotions',
16
-    'oscar.apps.checkout.context_processors.checkout',
76
+to allow customers to sign in using an email address rather than a username.
17 77
 
18
-to ``TEMPLATE_CONTEXT_PROCESSORS``.  Next, add the following apps
19
-to your ``INSTALLED_APPS``::
78
+Oscar currently uses Haystack for search so you need to specify::
20 79
 
21
-    'oscar',
22
-    'oscar.apps.analytics',
23
-    'oscar.apps.discount',
24
-    'oscar.apps.order',
25
-    'oscar.apps.checkout',
26
-    'oscar.apps.shipping',
27
-    'oscar.apps.order_management',
28
-    'oscar.apps.catalogue',
29
-    'oscar.apps.catalogue.reviews',
30
-    'oscar.apps.basket',
31
-    'oscar.apps.payment',
32
-    'oscar.apps.offer',
33
-    'oscar.apps.address',
34
-    'oscar.apps.partner',
35
-    'oscar.apps.customer',
36
-    'oscar.apps.promotions',
37
-    'oscar.apps.reports',
38
-    'oscar.apps.search',
39
-    'oscar.apps.voucher',
80
+    HAYSTACK_SITECONF = 'oscar.search_sites'
81
+    HAYSTACK_SEARCH_ENGINE = 'dummy'
40 82
 
41
-Add::
83
+The last addition to the settings file is to import all of Oscar's default settings::
42 84
 
43 85
     from oscar.defaults import *
44 86
 
45
-to your ``settings`` module and run::
87
+URLs
88
+----
89
+
90
+Alter your ``frobshop/urls.py`` to include Oscar's URLs::
91
+
92
+    from django.conf.urls import patterns, include, url
93
+    from oscar.app import shop
94
+
95
+    urlpatterns = ('',
96
+        (r'', include(shop.urls))
97
+    )
98
+
99
+Database
100
+--------
101
+
102
+Then create the database and the shop should be browsable::
103
+
104
+    python manage.py syncdb --noinput
105
+    python manage.py migrate
106
+
107
+You should now have a running Oscar install that you can browse.
108
+
109
+
110
+Install using Tangent's boilerplate django project
111
+==================================================
112
+
113
+The easiest way to get started is to use Tangent's `template django project`_
114
+although it is tailored to an Agency structure which may not suit everyone.
115
+
116
+.. `template django project`: https://github.com/tangentlabs/tangent-django-boilerplate
117
+
118
+Set up a virtualenv, and create a new project using the ``startproject``
119
+management command::
120
+
121
+    mkvirtualenv frobshop
122
+    pip install Django
123
+    django-admin.py startproject frobshop \
124
+        --template=https://github.com/tangentlabs/tangent-django-boilerplate/zipball/master 
46 125
 
47
-    python manage.py syncdb
126
+This will create a folder ``frobshop`` which is an entire templated project that
127
+follows Tangent's conventions.  The structure is:
48 128
 
49
-to create the database tables.
129
+    frobshop/
130
+        docs/
131
+        www/
132
+            conf/
133
+            deploy/
134
+            public/
135
+            static/
136
+            templates/
137
+            manage.py
138
+            settings.py
139
+            settings_test.py
140
+            urls.py
141
+            urls_oscar.py
142
+        README.rst
143
+        fabconfig.py
144
+        fabfile.py
145
+        deploy-to-test.sh
146
+        deploy-to-stage.sh
147
+        deploy-to-prod.sh
50 148
 
149
+Replace a few files with Oscar-specific versions::
51 150
 
52
-Demo shop
53
----------
151
+    mv frobshop/www/urls{_oscar,}.py
152
+    mv frobshop/www/deploy/requirements{_oscar,}.py
153
+    mv frobshop/www/conf/default{_oscar,}.py
54 154
 
55
-A demo shop is in preparation at the moment and will be available soon.
155
+Install dependencies::
56 156
 
57
-Real shop
58
----------
157
+    cd frobshop/www
158
+    pip install -r deploy/requirements.txt
59 159
 
60
-Sadly, setting up an e-commerce store is never trivial as you would like.  At a
61
-minimum, you'll have to consider the following questions:
160
+Create database::
62 161
 
63
-* How are shipping charges calculated?
64
-* How are products organised into categories?
65
-* How are stock messages determined?
66
-* How is payment taken at checkout?
67
-* How are orders fulfilled and managed?
68
-* How is stock and inventory updated?
162
+    python manage.py syncdb -noinput
163
+    python manage.py migrate
69 164
 
70
-Much of the documentation for oscar is organised as recipes that explain
71
-how to solve questions such as those above:
165
+And that should be it.
72 166
 
73
-* :doc:`recipes/how_to_customise_an_app`
74
-* :doc:`recipes/how_to_customise_models`
75
-* :doc:`recipes/how_to_override_a_core_class`
167
+Next steps
168
+==========
76 169
 
170
+The next step is to implement the business logic of your domain on top of
171
+Oscar.

+ 39
- 17
docs/source/index.rst 查看文件

@@ -2,38 +2,60 @@
2 2
    You can adapt this file completely to your liking, but it should at least
3 3
    contain the root `toctree` directive.
4 4
 
5
-==================================================
6
-django-oscar - Domain-driven e-commerce for Django
7
-==================================================
5
+.. image:: http://img94.imageshack.us/img94/9094/oscarza.jpg
8 6
 
9
-django-oscar is an e-commerce framework for Django designed for building
7
+===================================
8
+Domain-driven e-commerce for Django
9
+===================================
10
+
11
+Oscar is an e-commerce framework for Django designed for building
10 12
 domain-driven applications.  It is structured so that the core business objects
11
-(eg the models for a product/basket/order etc) can be customised to suit the
12
-domain at hand.  In this way, your application can accurately model its domain,
13
-making feature development and maintenance much easier.
13
+can be customised to suit the domain at hand.  In this way, your application
14
+can accurately model its domain, making feature development and maintenance
15
+much easier.
14 16
 
15
-This is in contrast to alternative e-commerce frameworks which use meta-data and
16
-key-value tables to model a domain.
17
+Features:
17 18
 
18
-Oscar is developed by `Tangent Labs`_, a London-based digital agency.  It is used in
19
-production in several applications to sell everything from beer mats to ipads.
19
+* Any product type can be handled, including downloadable products,
20
+  subscriptions, variant products (eg a T-shirt in different sizes and colours).
20 21
 
21
-.. _`Tangent Labs`: http://www.tangentlabs.co.uk
22
+* Customisable products, such as T-shirts with personalised messages.
22 23
 
23
-The `source is on Github`_. 
24
+* Can be used for large catalogues - Oscar is used in production by sites with
25
+  more than 20 million products.
24 26
 
25
-.. _`source is on Github`: https://github.com/tangentlabs/django-oscar
27
+* Multiple fulfillment partners for the same product.
28
+
29
+* Range of merchandising blocks for promoting products throughuout your site.
30
+
31
+* Sophisticated offers that support virtually any kind of offer you can think
32
+  of - multibuys, bundles, buy X get 50% of Y etc
33
+
34
+* Vouchers
26 35
 
27
-Table of contents
28
------------------
36
+* Comprehensive dashboard
37
+
38
+* Support for split payment orders
39
+
40
+* Extension libraries available for PayPal, GoCardless, DataCash and more
41
+
42
+Oscar is developed by `Tangent Labs`_, a London-based digital agency.  It is
43
+used in production in several applications to sell everything from beer mats to
44
+ipads.  The `source is on Github`_. 
45
+
46
+.. _`Tangent Labs`: http://www.tangentlabs.co.uk
47
+.. _`source is on Github`: https://github.com/tangentlabs/django-oscar
29 48
 
30 49
 .. toctree::
31 50
    :maxdepth: 2
32 51
 
52
+   take_a_peek
33 53
    getting_started
34
-   customisation
54
+   key_questions
35 55
    recipes
56
+   design_decisions
36 57
    reference
58
+   contributing
37 59
 
38 60
 Indices and tables
39 61
 ==================

+ 0
- 95
docs/source/introduction_to_ecommerce.rst 查看文件

@@ -1,95 +0,0 @@
1
-================
2
-eCommerce domain
3
-================
4
-
5
-When building an e-commerce site, there are several components whose
6
-implementation is strongly domain-specific.  That is, every site will have
7
-different requirements for how such a component should operate.  As such, these components
8
-cannot easily be modelled using a generic system - no configurable system will be able
9
-to accurately capture all the domain-specific behaviour required.  
10
-
11
-The design philosophy of oscar is to not make a decision for you here, but to
12
-provide the environment where any domain logic can be implemented, no matter
13
-how complex.  This is achieved through the use of subclassable objects that can 
14
-be tailored to your domain. 
15
-
16
-This document lists the components which will require implementation according to the
17
-domain:
18
-
19
-Taxonomy
20
---------
21
-How are products organised within the site?  A common pattern is to have a single 
22
-"category tree" where each product belongs to one category which sits within a tree structure
23
-of other categories?
24
-
25
-However, there are lots of other options such as having several separate taxonomy trees (eg split by
26
-brand, by theme, by product type).
27
-
28
-* Can a product belong to more than one category?
29
-* Can a category sit in more than one place within the tree?  (eg a "children's fiction" category
30
-  might sit beneath "children's books" and "fiction").
31
-
32
-Payment flow
33
-------------
34
-* Will the customer be debited at point of checkout, or when the items are dispatched?
35
-* If charging after checkout, when are shipping charges collected?  
36
-* What happens if an order is cancelled after partial payment?
37
-
38
-Payment sources
39
----------------
40
-How are customers going to pay for orders?  
41
-
42
-Will it be a simple, single-payment source solution such as paying by bankcard or using 
43
-Google checkout?  Or something more complicated such as allowing payment to be split across
44
-multiple payment sources such as a bankcard and a giftcard?
45
-
46
-More commonly, multiple payment sources can be used - such as:
47
-
48
-* Bankcard
49
-* Google checkout
50
-* PayPal
51
-* Business account
52
-* Managed budget
53
-* No upfront payment but send invoices later
54
-* Giftcard
55
-
56
-The checkout app within django-oscar is suitable flexible that all of these methods (and in 
57
-any combination) is supported.  However, you will need to implement the logic for your domain
58
-by subclassing the relevant view/util classes. 
59
-
60
-Domain logic is often required to: 
61
-
62
-* Determine which sources are available to an order;
63
-* Determine if payment can be split across sources and in which combinations;
64
-* Determine the order in which to take payment
65
-
66
-Stock logic
67
------------
68
-* Does the site support pre-orders (ordering before the product is available to be shipped) or
69
-  back-orders (ordering when the product does not have stock)?  
70
-
71
-Availability
72
-------------
73
-* Based on the stock information from a fulfilment partner, what messaging should be 
74
-  displayed on the site?  Further, should
75
-
76
-Shipping
77
---------
78
-Every client has a different requirement for shipping charges.  At its core, shipping charges
79
-normall depend on the following:
80
-
81
-* Items in basket
82
-* Shipping method chosen (e.g., standard or courier delivery)
83
-* Dispatch method chosen (e.g., ship together or ship separately)
84
-* Shipping address (e.g., which country it is in)
85
-* Basket vouchers (e.g., a voucher which gives free delivery)
86
-
87
-Common questions:
88
-
89
-* Are items shipping together as one batch, or separately?
90
-
91
-Checkout
92
---------
93
-
94
-
95
-

+ 167
- 0
docs/source/key_questions.rst 查看文件

@@ -0,0 +1,167 @@
1
+==============================================
2
+Building an e-commerce site: the key questions
3
+==============================================
4
+
5
+When building an e-commerce site, there are several components whose
6
+implementation is strongly domain-specific.  That is, every site will have
7
+different requirements for how such a component should operate.  As such, these
8
+components cannot easily be modelled using a generic system - no configurable
9
+system will be able to accurately capture all the domain-specific behaviour
10
+required.
11
+
12
+The design philosophy of Oscar is to not make a decision for you here, but to
13
+provide the environment where any domain logic can be implemented, no matter how
14
+complex.
15
+
16
+This document lists the components which will require implementation according
17
+to the domain at hand.  These are the key questions to answer when building your
18
+application.  Much of Oscar's documentation is in the form of "recipes" that
19
+explain how to solve the questions listed here.  Each question links to the
20
+relevant recipes.
21
+
22
+Catalogue
23
+=========
24
+
25
+What are your product types?
26
+----------------------------
27
+
28
+Are you selling books, DVDs, clothing, downloads or fruit and veg?  You will
29
+need to capture the attributes of your product types within your models.  Oscar
30
+divides products into 'product classes' which each have their own set of
31
+attributes.  
32
+
33
+* :doc:`recipes/how_to_model_your_catalogue`
34
+* :doc:`recipes/importing_a_catalogue`
35
+
36
+How is your catalogue organised?
37
+--------------------------------
38
+
39
+How are products organised within the site?  A common pattern is to have a
40
+single category tree where each product belongs to one category which sits
41
+within a tree structure of other categories.  However, there are lots of other
42
+options such as having several separate taxonomy trees (eg split by brand, by
43
+theme, by product type).  Other questions to consider:
44
+
45
+* Can a product belong to more than one category?
46
+* Can a category sit in more than one place within the tree?  (eg a "children's fiction" category
47
+  might sit beneath "children's books" and "fiction").
48
+
49
+* :doc:`recipes/how_to_customise_an_app`
50
+* :doc:`recipes/how_to_customise_models`
51
+* :doc:`recipes/how_to_override_a_core_class`
52
+
53
+How are products managed?
54
+-------------------------
55
+
56
+Is the catalogue managed by a admin using a dashboard, or though an automated
57
+process, such as processing feeds from a fulfillment system?  Where are your
58
+product images going to be served from?
59
+
60
+* :doc:`recipes/how_to_disable_an_app`
61
+
62
+
63
+Pricing, stock and availability
64
+===============================
65
+
66
+How is tax calculated?
67
+----------------------
68
+
69
+What availability messages are shown to customers?
70
+--------------------------------------------------
71
+
72
+Based on the stock information from a fulfilment partner, what messaging should be
73
+displayed on the site?  
74
+
75
+* :doc:`recipes/how_to_configure_stock_messaging`
76
+
77
+Do you allow pre- and back-orders
78
+---------------------------------
79
+
80
+An pre-order is where you allow a product to be bought before it has been
81
+published, while a back-order is where you allow a product to be bought that is
82
+currently out of stock.
83
+
84
+
85
+Shipping
86
+========
87
+
88
+How are shipping charges calculated?
89
+------------------------------------
90
+
91
+There are lots of options and variations here.  Shipping methods and their
92
+associated charges can take a variety of forms, including:
93
+
94
+* A charge based on the weight of the basket
95
+* Charging a pre-order and pre-item charge
96
+* Having free shipping for orders above a given threshold
97
+
98
+Recipes:
99
+
100
+* :doc:`recipes/how_to_configure_shipping`
101
+
102
+Which shipping methods are available?
103
+-------------------------------------
104
+
105
+There's often also an issue of which shipping methods are available, as
106
+this can depend on:
107
+
108
+* The shipping address (eg overseas orders have higher charges)
109
+* The contents of the basket (eg free shipping for downloadable products)
110
+* Who the user is (eg sales reps get free shipping)
111
+
112
+Oscar provides classes for free shipping, fixed charge shipping, pre-order and
113
+per-product item charges and weight-based charges.  It is provides a mechanism
114
+for determing which shipping methods are available to the user.
115
+
116
+Recipes:
117
+
118
+* :doc:`recipes/how_to_configure_shipping`
119
+
120
+
121
+Payment
122
+=======
123
+
124
+How are customers going to pay for orders?
125
+------------------------------------------
126
+
127
+Often a shop will have a single mechanism for taking payment, such
128
+as integrating with a payment gateway or using PayPal.  However more
129
+complicated projects will allow users to combine several different payment
130
+sources such as bankcards, business accounts and giftcards.
131
+
132
+Possible payment sources include:
133
+
134
+* Bankcard
135
+* Google checkout
136
+* PayPal
137
+* Business account
138
+* Managed budget
139
+* Giftcard
140
+* No upfront payment but send invoices later
141
+
142
+The checkout app within django-oscar is suitable flexible that all of these
143
+methods (and in any combination) is supported.  However, you will need to
144
+implement the logic for your domain by subclassing the relevant view/util
145
+classes.
146
+
147
+Domain logic is often required to:
148
+
149
+* Determine which payment methods are available to an order;
150
+* Determine if payment can be split across sources and in which combinations;
151
+* Determine the order in which to take payment
152
+* Determine how to handle failing payments (this can get complicated when using
153
+  multiple payment sources to pay for an order).
154
+
155
+* :doc:`recipes/how_to_configure_shipping`
156
+
157
+When will payment be taken?
158
+---------------------------
159
+
160
+A common pattern is to 'pre-auth' a bankcard at the point of checkout then
161
+'settle' for the appropriate amouts when the items actually ship.  However,
162
+sometimes payment is taken up front.  Often you won't have a choice due to
163
+limitations of the payment partner you need to integrate with.
164
+
165
+* Will the customer be debited at point of checkout, or when the items are dispatched?
166
+* If charging after checkout, when are shipping charges collected?
167
+* What happens if an order is cancelled after partial payment?

+ 30
- 9
docs/source/recipes.rst 查看文件

@@ -1,5 +1,6 @@
1
+=======
1 2
 Recipes
2
-============
3
+=======
3 4
 
4 5
 Recipes are simple guides to solving common problems that occur when creating
5 6
 e-commerce projects.
@@ -7,19 +8,39 @@ e-commerce projects.
7 8
 Customisation
8 9
 -------------
9 10
 
10
-* :doc:`recipes/how_to_customise_an_app`
11
-* :doc:`recipes/how_to_customise_models`
12
-* :doc:`recipes/how_to_override_a_core_class`
11
+.. toctree::
12
+    :maxdepth: 1
13
+
14
+    recipes/how_to_customise_an_app
15
+    recipes/how_to_customise_models
16
+    recipes/how_to_override_a_core_class
17
+    recipes/how_to_customise_templates
18
+    recipes/how_to_disable_an_app
13 19
 
14 20
 Catalogue
15 21
 ---------
16 22
 
17
-* :doc:`recipes/how_to_create_categories`
23
+.. toctree::
24
+    :maxdepth: 1
25
+
26
+    recipes/how_to_create_categories
27
+    recipes/how_to_model_your_catalogue
28
+    recipes/importing_a_catalogue
29
+
30
+Pricing, stock and availability
31
+-------------------------------
18 32
 
19
-Checkout
33
+.. toctree::
34
+    :maxdepth: 1
35
+
36
+    recipes/enforcing_stock_rules
37
+    recipes/how_to_configure_stock_messaging
38
+
39
+Shipping
20 40
 --------
21 41
 
22
-* :doc:`recipes/how_to_configure_shipping`
23
-* :doc:`recipes/enforcing_stock_rules`
42
+.. toctree::
43
+    :maxdepth: 1
44
+
45
+    recipes/how_to_configure_shipping
24 46
 
25
-Lots more to come!

+ 4
- 3
docs/source/recipes/how_to_configure_shipping.rst 查看文件

@@ -56,9 +56,10 @@ with a custom repository.
56 56
 
57 57
 * ``oscar.apps.shipping.methods.Free``.  No shipping charges.
58 58
 
59
-* ``oscar.apps.shipping.methods.WeightBased``.  This uses a model ``WeightBand``
60
-  to provide charges for different weight bands.  By default, the method will calculate
61
-  the weight of a product by looking for a 'weight' attribute although this can be
59
+* ``oscar.apps.shipping.methods.WeightBased``.  This is a model-driven method
60
+  that uses two models: ``WeightBased`` and ``WeightBand`` to provide charges
61
+  for different weight bands.  By default, the method will calculate the weight
62
+  of a product by looking for a 'weight' attribute although this can be
62 63
   configured.  
63 64
 
64 65
 * ``oscar.apps.shipping.methods.FixedPrice``.  This simply charges a fixed price for 

+ 3
- 0
docs/source/recipes/how_to_configure_stock_messaging.rst 查看文件

@@ -0,0 +1,3 @@
1
+================================
2
+How to configure stock messaging
3
+================================

+ 82
- 0
docs/source/recipes/how_to_customise_templates.rst 查看文件

@@ -0,0 +1,82 @@
1
+==========================
2
+How to customise templates
3
+==========================
4
+
5
+Assuming you want to use oscar's templates in your project, there are two
6
+options.  You don't have to though - you could write all your own templates if
7
+you like.  If you do this, it's probably best to start with a straight copy of
8
+all of oscar's templates so you know all the files that you need to
9
+re-implement.
10
+
11
+Anyway - here are the two options for customising.
12
+
13
+Method 1 - Forking
14
+------------------
15
+
16
+One option is always just to fork the template into your local project so that
17
+it comes first in the include path.
18
+
19
+Say you want to customise ``base.html``.  First you need a project-specific
20
+templates directory that comes first in the include path.  You can set this up
21
+as so::
22
+
23
+    TEMPLATE_LOADERS = (
24
+        'django.template.loaders.filesystem.Loader',
25
+        'django.template.loaders.app_directories.Loader',
26
+    )
27
+
28
+    import os
29
+    location = lambda x: os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', x)
30
+    TEMPLATE_DIRS = (
31
+        location('templates'),
32
+    )
33
+
34
+Next copy oscar's ``base.html`` into your templates directory and customise it
35
+to suit your needs.
36
+
37
+The downsides of this method are that it involves duplicating the file from
38
+oscar in a way that breaks the link with upstream.  Hence, changes to oscar's
39
+``base.html`` won't be picked up by your project as you will have your own
40
+version.
41
+
42
+Method 2 - Subclass parent but use same template path
43
+-----------------------------------------------------
44
+
45
+There is a trick you can perform whereby oscar's templates can be accessed via
46
+two paths.  This is outlined in the `django wiki`_.
47
+
48
+.. _`django wiki`: https://code.djangoproject.com/wiki/ExtendingTemplates
49
+
50
+This basically means you can have a ``base.html`` in your local templates folder
51
+that extends oscar's ``base.html`` but only customises the blocks that it needs
52
+to.
53
+
54
+Oscar provides a helper variable to make this easy.  First, set up your
55
+template configuration as so::
56
+
57
+    TEMPLATE_LOADERS = (
58
+        'django.template.loaders.filesystem.Loader',
59
+        'django.template.loaders.app_directories.Loader',
60
+    )
61
+
62
+    import os
63
+    location = lambda x: os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', x)
64
+    from oscar import OSCAR_PARENT_TEMPLATE_DIR
65
+    TEMPLATE_DIRS = (
66
+        location('templates'),
67
+        OSCAR_PARENT_TEMPLATE_DIR,
68
+    )
69
+
70
+The ``OSCAR_PARENT_TEMPLATE_DIR`` points to the directory above oscar's normal
71
+templates directory.  This means that ``path/to/oscar/template.html`` can also
72
+be reached via ``templates/path/to/oscar/template.html``.
73
+
74
+Hence to customise ``base.html``, you can have an implementation like::
75
+
76
+    # base.html
77
+    {% extends 'templates/base.html' %}
78
+
79
+    ...
80
+
81
+No real downsides to this one other than getting your front-end people to
82
+understand it.

+ 6
- 0
docs/source/recipes/how_to_disable_an_app.rst 查看文件

@@ -0,0 +1,6 @@
1
+=====================
2
+How to disable an app
3
+=====================
4
+
5
+...
6
+

+ 10
- 0
docs/source/recipes/how_to_model_your_catalogue.rst 查看文件

@@ -0,0 +1,10 @@
1
+===========================
2
+How to model your catalogue
3
+===========================
4
+
5
+
6
+
7
+Related recipes:
8
+
9
+* :doc:`recipes/how_to_customise_an_app`
10
+* :doc:`recipes/how_to_customise_models`

+ 6
- 0
docs/source/recipes/importing_a_catalogue.rst 查看文件

@@ -0,0 +1,6 @@
1
+=====================
2
+Importing a catalogue
3
+=====================
4
+
5
+...
6
+

+ 1
- 0
docs/source/reference.rst 查看文件

@@ -8,3 +8,4 @@ Contents:
8 8
    :maxdepth: 2
9 9
 
10 10
    reference/settings
11
+   reference/signals

+ 23
- 0
docs/source/reference/signals.rst 查看文件

@@ -0,0 +1,23 @@
1
+=======
2
+Signals
3
+=======
4
+
5
+Oscar defined a number of custom signals that provide useful hook-points for
6
+adding functionality.
7
+
8
+order_placed
9
+------------
10
+
11
+.. data:: oscar.apps.order.order_placed
12
+    :class:
13
+
14
+Raised by the :class:`oscar.apps.order.OrderCreator` class when creating an order.
15
+
16
+Arguments sent with this signal:
17
+
18
+``order``
19
+    The order created
20
+
21
+``user``
22
+    The user creating the order (not necessarily the user linked to the order
23
+    instance!)

+ 80
- 0
docs/source/take_a_peek.rst 查看文件

@@ -0,0 +1,80 @@
1
+===========
2
+Take a peek
3
+===========
4
+
5
+There are several ways to get a feel for Oscar and what it can do.
6
+
7
+Browse the sandbox site
8
+=======================
9
+
10
+There is a demo Oscar site, built hourly from HEAD of the master branch (unstable):
11
+http://sandbox.oscar.tangentlabs.co.uk
12
+
13
+It is intended to be a vanilla install of oscar, using the default templates and
14
+styles.  This is the blank canvas upon which you an build your application.
15
+
16
+It only has two customisations on top of oscar's core:
17
+
18
+* Two shipping methods are specified so that the shipping method step of
19
+  checkout is not skipped.  If there is only one shipping method (which is true of core
20
+  oscar) then the shipping method step is skipped as there is no choice to be
21
+  made.
22
+
23
+* A profile class is specified which defines a few simple fields.  This is to
24
+  demonstrate the account section of Oscar, which introspects the profile class
25
+  to build a combined User and Profile form.
26
+
27
+Running the sandbox locally
28
+===========================
29
+
30
+It's pretty straightforward to get the sandbox site running locally so you can
31
+play around with the sourcecode.
32
+
33
+Install Oscar and its dependencies within a virtualenv::
34
+
35
+    git clone git://github.com/tangentlabs/django-oscar.git
36
+    cd django-oscar
37
+    mkvirtualenv oscar
38
+    python setup.py develop
39
+    pip install -r requirements.txt
40
+
41
+Create a SQLite database and load some sample products and required fixtures::
42
+
43
+    cd sandbox
44
+    ./manage.py syncdb --noinput
45
+    ./manage.py migrate 
46
+    ./manage.py oscar_import_catalogue data/books-catalogue.csv 
47
+    ./manage.py oscar_import_catalogue_images data/books-images.tar.gz 
48
+    ./manage.py loaddata countries.json fixtures/pages.json 
49
+
50
+Now you can browse a sample Oscar site using Django's development server::
51
+
52
+    ./manage.py runserver
53
+
54
+Note that some things are deliberately not implemented within core Oscar
55
+as they are domain-specific.  For instance:
56
+
57
+* All tax is set to zero
58
+* The two shipping methods are both free
59
+* No payment is required to submit an order
60
+
61
+I've found a problem!
62
+---------------------
63
+
64
+Please `report them in Github's issue tracker`_.
65
+
66
+.. _`report them in Github's issue tracker`: https://github.com/tangentlabs/django-oscar/issues
67
+
68
+Browse some real Oscar implementations
69
+======================================
70
+
71
+There are several Oscar implementations in production, although several are B2B
72
+and hence can't be browsed by the public.  Here's a few public ones to have a
73
+look at:
74
+
75
+* http://www.landmarkonthenet.com - Landmark is part of Tata Group.  This site
76
+  has a catalogue size of more than 20 million products and integrates with many
77
+  partners such as Gardners, Ingram, Nielsen, Citibank, Qwikcilver and SAP.
78
+
79
+* http://www.dolbeau.ca/ - "Dolbeau delivers weekly limited editions of
80
+  handcrafted luxury menswear"

+ 3
- 0
docs/test_docs.sh 查看文件

@@ -0,0 +1,3 @@
1
+#!/usr/bin/env bash
2
+make html
3
+open build/html/index.html &

+ 61
- 32
oscar/apps/analytics/reports.py 查看文件

@@ -6,43 +6,57 @@ from django.template.defaultfilters import date
6 6
 from oscar.core.loading import get_class
7 7
 
8 8
 ReportGenerator = get_class('dashboard.reports.reports', 'ReportGenerator')
9
+ReportCSVFormatter = get_class('dashboard.reports.reports', 'ReportCSVFormatter')
10
+ReportHTMLFormatter = get_class('dashboard.reports.reports', 'ReportHTMLFormatter')
9 11
 ProductRecord = get_model('analytics', 'ProductRecord')
10 12
 UserRecord = get_model('analytics', 'UserRecord')
11 13
 
12 14
 
13
-class ProductReportGenerator(ReportGenerator):
14
-    
15
-    filename_template = 'product-analytics.csv'
16
-    code = 'product_analytics'
17
-    description = 'Product analytics'
18
-    
19
-    def generate(self, response):
15
+class ProductReportCSVFormatter(ReportCSVFormatter):
16
+    filename_template = 'conditional-offer-performance.csv'
17
+
18
+    def generate_csv(self, response, products):
20 19
         writer = csv.writer(response)
21 20
         header_row = ['Product',
22 21
                       'Views',
23 22
                       'Basket additions',
24 23
                       'Purchases',]
25 24
         writer.writerow(header_row)
26
-        
27
-        records = ProductRecord._default_manager.all()
28
-        for record in records:
29
-            row = [record.product, record.num_views, record.num_basket_additions, record.num_purchases]
25
+
26
+        for record in products:
27
+            row = [record.product,
28
+                   record.num_views,
29
+                   record.num_basket_additions,
30
+                   record.num_purchases]
30 31
             writer.writerow(row)
31
-            
32
+
33
+
34
+class ProductReportHTMLFormatter(ReportHTMLFormatter):
35
+    filename_template = 'dashboard/reports/partials/product_report.html'
36
+
37
+
38
+class ProductReportGenerator(ReportGenerator):
39
+
40
+    code = 'product_analytics'
41
+    description = 'Product analytics'
42
+
43
+    formatters = {
44
+      'CSV_formatter': ProductReportCSVFormatter,
45
+      'HTML_formatter': ProductReportHTMLFormatter
46
+    }
47
+
48
+    def generate(self):
49
+        records = ProductRecord._default_manager.all()
50
+        return self.formatter.generate_response(records)
51
+
32 52
     def is_available_to(self, user):
33 53
         return user.is_staff
34
-    
35
-    def filename(self):
36
-        return self.filename_template
37
-    
38 54
 
39
-class UserReportGenerator(ReportGenerator):
40
-    
55
+
56
+class UserReportCSVFormatter(ReportCSVFormatter):
41 57
     filename_template = 'user-analytics.csv'
42
-    code = 'user_analytics'
43
-    description = 'User analytics'
44
-    
45
-    def generate(self, response):
58
+
59
+    def generate_csv(self, response, users):
46 60
         writer = csv.writer(response)
47 61
         header_row = ['Name',
48 62
                       'Date registered',
@@ -55,22 +69,37 @@ class UserReportGenerator(ReportGenerator):
55 69
                       'Date of last order',
56 70
                       ]
57 71
         writer.writerow(header_row)
58
-        
59
-        records = UserRecord._default_manager.select_related().all()
60
-        for record in records:
61
-            row = [record.user.get_full_name(), 
72
+
73
+        for record in users:
74
+            row = [record.user.get_full_name(),
62 75
                    self.format_date(record.user.date_joined),
63
-                   record.num_product_views, 
64
-                   record.num_basket_additions, 
76
+                   record.num_product_views,
77
+                   record.num_basket_additions,
65 78
                    record.num_orders,
66 79
                    record.num_order_lines,
67 80
                    record.num_order_items,
68 81
                    record.total_spent,
69 82
                    self.format_datetime(record.date_last_order)]
70 83
             writer.writerow(row)
71
-            
84
+
85
+
86
+class UserReportHTMLFormatter(ReportHTMLFormatter):
87
+    filename_template = 'dashboard/reports/partials/user_report.html'
88
+
89
+
90
+class UserReportGenerator(ReportGenerator):
91
+
92
+    code = 'user_analytics'
93
+    description = 'User analytics'
94
+
95
+    formatters = {
96
+        'CSV_formatter': UserReportCSVFormatter,
97
+        'HTML_formatter': UserReportHTMLFormatter
98
+    }
99
+
100
+    def generate(self):
101
+        users = UserRecord._default_manager.select_related().all()
102
+        return self.formatter.generate_response(users)
103
+
72 104
     def is_available_to(self, user):
73 105
         return user.is_staff
74
-    
75
-    def filename(self):
76
-        return self.filename_template

+ 67
- 21
oscar/apps/basket/reports.py 查看文件

@@ -4,19 +4,16 @@ from django.db.models import get_model
4 4
 
5 5
 from oscar.core.loading import get_class, get_classes
6 6
 ReportGenerator = get_class('dashboard.reports.reports', 'ReportGenerator')
7
+ReportCSVFormatter = get_class('dashboard.reports.reports', 'ReportCSVFormatter')
8
+ReportHTMLFormatter = get_class('dashboard.reports.reports', 'ReportHTMLFormatter')
7 9
 Basket = get_model('basket', 'Basket')
8 10
 OPEN, SUBMITTED = get_classes('basket.models', ['OPEN', 'SUBMITTED'])
9 11
 
10 12
 
11
-class OpenBasketReportGenerator(ReportGenerator):
12
-    """
13
-    Report of baskets which haven't been submitted yet
14
-    """
13
+class OpenBasketReportCSVFormatter(ReportCSVFormatter):
15 14
     filename_template = 'open-baskets-%s-%s.csv'
16
-    code = 'open_baskets'
17
-    description = 'Open baskets'
18
-    
19
-    def generate(self, response):
15
+
16
+    def generate_csv(self, response, baskets):
20 17
         writer = csv.writer(response)
21 18
         header_row = ['User ID',
22 19
                       'Name',
@@ -29,31 +26,53 @@ class OpenBasketReportGenerator(ReportGenerator):
29 26
                       'Time since creation',
30 27
                      ]
31 28
         writer.writerow(header_row)
32
-        
33
-        baskets = Basket._default_manager.filter(status=OPEN)
29
+
34 30
         for basket in baskets:
35 31
             if basket.owner:
36 32
                 row = [basket.owner_id, basket.owner.get_full_name(), basket.owner.email,
37 33
                        basket.status, basket.num_lines,
38
-                       basket.num_items, basket.total_incl_tax, 
39
-                       self.format_datetime(basket.date_created), 
34
+                       basket.num_items, basket.total_incl_tax,
35
+                       self.format_datetime(basket.date_created),
40 36
                        basket.time_since_creation]
41 37
             else:
42 38
                 row = [basket.owner_id, None, None, basket.status, basket.num_lines,
43
-                       basket.num_items, basket.total_incl_tax, 
39
+                       basket.num_items, basket.total_incl_tax,
44 40
                        self.format_datetime(basket.date_created), basket.time_since_creation]
45 41
             writer.writerow(row)
46 42
 
43
+    def filename(self, **kwargs):
44
+        return self.filename_template % (kwargs['start_date'], kwargs['end_date'])
47 45
 
48
-class SubmittedBasketReportGenerator(ReportGenerator):
46
+
47
+class OpenBasketReportHTMLFormatter(ReportHTMLFormatter):
48
+    filename_template = 'dashboard/reports/partials/open_basket_report.html'
49
+
50
+
51
+class OpenBasketReportGenerator(ReportGenerator):
49 52
     """
50
-    Report of baskets that have been submitted
53
+    Report of baskets which haven't been submitted yet
51 54
     """
55
+    code = 'open_baskets'
56
+    description = 'Open baskets'
57
+
58
+    formatters = {
59
+        'CSV_formatter': OpenBasketReportCSVFormatter,
60
+        'HTML_formatter': OpenBasketReportHTMLFormatter
61
+    }
62
+
63
+    def generate(self):
64
+        additional_data = {
65
+            'start_date': self.start_date,
66
+            'end_date': self.end_date
67
+        }
68
+        baskets = Basket._default_manager.filter(status=OPEN)
69
+        return self.formatter.generate_response(baskets, **additional_data)
70
+
71
+
72
+class SubmittedBasketReportCSVFormatter(ReportCSVFormatter):
52 73
     filename_template = 'submitted_baskets-%s-%s.csv'
53
-    code = 'submitted_baskets'
54
-    description = 'Submitted baskets'
55
-    
56
-    def generate(self, response):
74
+
75
+    def generate_csv(self, response, baskets):
57 76
         writer = csv.writer(response)
58 77
         header_row = ['User ID',
59 78
                       'User',
@@ -65,8 +84,7 @@ class SubmittedBasketReportGenerator(ReportGenerator):
65 84
                       'Time between creation and submission',
66 85
                      ]
67 86
         writer.writerow(header_row)
68
-        
69
-        baskets = Basket._default_manager.filter(status=SUBMITTED)
87
+
70 88
         for basket in baskets:
71 89
             row = [basket.owner_id,
72 90
                    basket.owner,
@@ -77,3 +95,31 @@ class SubmittedBasketReportGenerator(ReportGenerator):
77 95
                    self.format_datetime(basket.date_created),
78 96
                    basket.time_before_submit]
79 97
             writer.writerow(row)
98
+
99
+    def filename(self, **kwargs):
100
+        return self.filename_template % (kwargs['start_date'], kwargs['end_date'])
101
+
102
+
103
+class SubmittedBasketReportHTMLFormatter(ReportHTMLFormatter):
104
+    filename_template = 'dashboard/reports/partials/submitted_basket_report.html'
105
+
106
+
107
+class SubmittedBasketReportGenerator(ReportGenerator):
108
+    """
109
+    Report of baskets that have been submitted
110
+    """
111
+    code = 'submitted_baskets'
112
+    description = 'Submitted baskets'
113
+
114
+    formatters = {
115
+        'CSV_formatter': SubmittedBasketReportCSVFormatter,
116
+        'HTML_formatter': SubmittedBasketReportHTMLFormatter
117
+    }
118
+
119
+    def generate(self):
120
+        additional_data = {
121
+            'start_date': self.start_date,
122
+            'end_date': self.end_date
123
+        }
124
+        baskets = Basket._default_manager.filter(status=SUBMITTED)
125
+        return self.formatter.generate_response(baskets, **additional_data)

+ 15
- 8
oscar/apps/basket/tests.py 查看文件

@@ -1,5 +1,6 @@
1 1
 from decimal import Decimal as D
2 2
 import httplib
3
+import datetime
3 4
 
4 5
 from django.conf import settings
5 6
 from django.contrib.auth.models import User
@@ -111,14 +112,22 @@ class BasketThresholdTest(TestCase):
111 112
 class BasketReportTests(TestCase):
112 113
 
113 114
     def test_open_report_doesnt_error(self):
114
-        generator = OpenBasketReportGenerator()
115
-        response = HttpResponse()
116
-        generator.generate(response)
115
+        data = {
116
+            'start_date': datetime.date(2012, 5, 1),
117
+            'end_date': datetime.date(2012, 5, 17),
118
+            'formatter': 'CSV'
119
+        }
120
+        generator = OpenBasketReportGenerator(**data)
121
+        generator.generate()
117 122
 
118 123
     def test_submitted_report_doesnt_error(self):
119
-        generator = SubmittedBasketReportGenerator()
120
-        response = HttpResponse()
121
-        generator.generate(response)
124
+        data = {
125
+            'start_date': datetime.date(2012, 5, 1),
126
+            'end_date': datetime.date(2012, 5, 17),
127
+            'formatter': 'CSV'
128
+        }
129
+        generator = SubmittedBasketReportGenerator(**data)
130
+        generator.generate()
122 131
 
123 132
 
124 133
 class SavedBasketTests(TestCase):
@@ -178,5 +187,3 @@ class SavedBasketTests(TestCase):
178 187
         # we can't add more than stock level into basket
179 188
         self.assertEqual(Basket.open.get(id=basket.id).lines.get(product=product).quantity, 1)
180 189
         self.assertRedirects(response, reverse('basket:summary'))
181
-
182
-

+ 3
- 3
oscar/apps/dashboard/reports/forms.py 查看文件

@@ -6,16 +6,16 @@ GeneratorRepository = get_class('dashboard.reports.utils', 'GeneratorRepository'
6 6
 
7 7
 class ReportForm(forms.Form):
8 8
     generators = GeneratorRepository().get_report_generators()
9
-    
9
+
10 10
     type_choices = []
11 11
     for generator in generators:
12 12
         type_choices.append((generator.code, generator.description))
13 13
     report_type = forms.ChoiceField(widget=forms.Select(), choices=type_choices)
14 14
     date_from = forms.DateField()
15 15
     date_to = forms.DateField()
16
-    
16
+    download = forms.BooleanField(required=False)
17
+
17 18
     def clean(self):
18 19
         if 'date_from' in self.cleaned_data and 'date_to' in self.cleaned_data and self.cleaned_data['date_from'] > self.cleaned_data['date_to']:
19 20
             raise forms.ValidationError("Your start date must be before your end date")
20 21
         return self.cleaned_data
21
-   

+ 30
- 7
oscar/apps/dashboard/reports/reports.py 查看文件

@@ -1,38 +1,61 @@
1 1
 from django.template.defaultfilters import date
2
+from django.http import HttpResponse
2 3
 
3 4
 
4 5
 class ReportGenerator(object):
5 6
     """
6
-    Top-level class that needs to be subclassed to provide a 
7
+    Top-level class that needs to be subclassed to provide a
7 8
     report generator.
8 9
     """
9 10
     filename_template = 'report-%s-to-%s.csv'
10 11
     mimetype = 'text/csv'
11 12
     code = ''
12 13
     description = '<insert report description>'
13
-    
14
+
14 15
     def __init__(self, **kwargs):
15 16
         if 'start_date' in kwargs and 'end_date' in kwargs:
16 17
             self.start_date = kwargs['start_date']
17 18
             self.end_date = kwargs['end_date']
18
-    
19
+
20
+        self.formatter = self.formatters['%s_formatter' % kwargs['formatter']]()
21
+
19 22
     def generate(self, response):
20 23
         pass
21
- 
24
+
22 25
     def filename(self):
23 26
         """
24 27
         Returns the filename for this report
25 28
         """
26
-        return self.filename_template % (self.start_date, self.end_date)
27
-    
29
+        return self.formatter.filename()
30
+
28 31
     def is_available_to(self, user):
29 32
         """
30 33
         Checks whether this report is available to this user
31 34
         """
32 35
         return user.is_staff
33 36
 
37
+
38
+class ReportFormatter(object):
34 39
     def format_datetime(self, dt):
35 40
         return date(dt, 'DATETIME_FORMAT')
36 41
 
37 42
     def format_date(self, d):
38
-        return date(d, 'DATE_FORMAT')
43
+        return date(d, 'DATE_FORMAT')
44
+
45
+    def filename(self):
46
+        return self.filename_template
47
+
48
+
49
+class ReportCSVFormatter(ReportFormatter):
50
+
51
+    def generate_response(self, objects, **kwargs):
52
+        response = HttpResponse(mimetype='text/csv')
53
+        response['Content-Disposition'] = 'attachment; filename=%s' % self.filename(**kwargs)
54
+        self.generate_csv(response, objects)
55
+        return response
56
+
57
+
58
+class ReportHTMLFormatter(ReportFormatter):
59
+
60
+    def generate_response(self, objects, **kwargs):
61
+        return objects

+ 4
- 4
oscar/apps/dashboard/reports/utils.py 查看文件

@@ -8,7 +8,7 @@ VoucherReportGenerator = get_class('voucher.reports', 'VoucherReportGenerator')
8 8
 
9 9
 
10 10
 class GeneratorRepository(object):
11
-    
11
+
12 12
     generators = [OrderReportGenerator,
13 13
                   ProductReportGenerator,
14 14
                   UserReportGenerator,
@@ -19,11 +19,11 @@ class GeneratorRepository(object):
19 19
 
20 20
     def get_report_generators(self):
21 21
         return self.generators
22
-    
22
+
23 23
     def get_generator(self, code):
24 24
         for generator in self.generators:
25 25
             if generator.code == code:
26 26
                 return generator
27 27
         return None
28
-    
29
-    
28
+
29
+

+ 34
- 12
oscar/apps/dashboard/reports/views.py 查看文件

@@ -1,31 +1,48 @@
1
-from django.http import HttpResponse, HttpResponseForbidden, Http404
1
+from django.http import HttpResponseForbidden, Http404
2 2
 from django.template.response import TemplateResponse
3
-from django.views.generic import TemplateView
3
+from django.views.generic import ListView
4 4
 
5 5
 from oscar.core.loading import get_class
6 6
 ReportForm = get_class('dashboard.reports.forms', 'ReportForm')
7 7
 GeneratorRepository = get_class('dashboard.reports.utils', 'GeneratorRepository')
8 8
 
9 9
 
10
-class IndexView(TemplateView):
10
+class IndexView(ListView):
11 11
     template_name = 'dashboard/reports/index.html'
12
-    
12
+    paginate_by = 25
13
+    context_object_name = 'objects'
14
+
13 15
     def get(self, request, *args, **kwargs):
14 16
         if 'report_type' in request.GET:
15 17
             form = ReportForm(request.GET)
16 18
             if form.is_valid():
17 19
                 generator = _get_generator(form)
18 20
                 if not generator.is_available_to(request.user):
19
-                    return HttpResponseForbidden("You do not have access to this report")
20
-                
21
-                response = HttpResponse(mimetype=generator.mimetype)
22
-                response['Content-Disposition'] = 'attachment; filename=%s' % generator.filename()
23
-                generator.generate(response)
24
-                return response
21
+                    return HttpResponseForbidden("You do not have access"
22
+                                                 " to this report")
23
+
24
+                report = generator.generate()
25
+
26
+                if form.cleaned_data['download']:
27
+                    return report
28
+                else:
29
+                    self.set_list_view_attrs(generator, report)
30
+                    context = self.get_context_data(object_list=self.queryset)
31
+                    context['form'] = form
32
+                    context['description'] = '%s between %s and %s' % (
33
+                        generator.description,
34
+                        form.cleaned_data['date_from'],
35
+                        form.cleaned_data['date_to'],
36
+                    )
37
+                    return self.render_to_response(context)
25 38
         else:
26 39
             form = ReportForm()
27 40
         return TemplateResponse(request, self.template_name, {'form': form})
28 41
 
42
+    def set_list_view_attrs(self, generator, report):
43
+        self.template_name = generator.filename()
44
+        self.object_list = self.queryset = report
45
+
29 46
 
30 47
 def _get_generator(form):
31 48
     code = form.cleaned_data['report_type']
@@ -34,5 +51,10 @@ def _get_generator(form):
34 51
     generator_cls = repo.get_generator(code)
35 52
     if not generator_cls:
36 53
         raise Http404()
37
-    return generator_cls(start_date=form.cleaned_data['date_from'], 
38
-                         end_date=form.cleaned_data['date_to'])
54
+
55
+    download = form.cleaned_data['download']
56
+    formatter = 'CSV' if download else 'HTML'
57
+
58
+    return generator_cls(start_date=form.cleaned_data['date_from'],
59
+                         end_date=form.cleaned_data['date_to'],
60
+                         formatter=formatter)

+ 42
- 10
oscar/apps/offer/reports.py 查看文件

@@ -1,28 +1,60 @@
1 1
 import csv
2
+from decimal import Decimal as D
2 3
 
3 4
 from django.db.models import get_model
4 5
 
5 6
 from oscar.core.loading import get_class
6 7
 ReportGenerator = get_class('dashboard.reports.reports', 'ReportGenerator')
8
+ReportCSVFormatter = get_class('dashboard.reports.reports', 'ReportCSVFormatter')
9
+ReportHTMLFormatter = get_class('dashboard.reports.reports', 'ReportHTMLFormatter')
7 10
 ConditionalOffer = get_model('offer', 'ConditionalOffer')
11
+OrderDiscount = get_model('order', 'OrderDiscount')
8 12
 
9 13
 
10
-class OfferReportGenerator(ReportGenerator):
11
-    
14
+class OfferReportCSVFormatter(ReportCSVFormatter):
12 15
     filename_template = 'conditional-offer-performance.csv'
13
-    code = 'conditional-offers'
14
-    description = 'Offer performance'
15
-    
16
-    def generate(self, response):
16
+
17
+    def generate_csv(self, response, offers):
17 18
         writer = csv.writer(response)
18 19
         header_row = ['Offer',
19 20
                       'Total discount',
20 21
                      ]
21 22
         writer.writerow(header_row)
22
-        
23
-        for offer in ConditionalOffer._default_manager.all():
23
+
24
+        for offer in offers:
24 25
             row = [offer, offer.total_discount]
25 26
             writer.writerow(row)
26 27
 
27
-    def filename(self):
28
-        return self.filename_template
28
+
29
+class OfferReportHTMLFormatter(ReportHTMLFormatter):
30
+    filename_template = 'dashboard/reports/partials/offer_report.html'
31
+
32
+
33
+class OfferReportGenerator(ReportGenerator):
34
+    code = 'conditional-offers'
35
+    description = 'Offer performance'
36
+
37
+    formatters = {
38
+        'CSV_formatter': OfferReportCSVFormatter,
39
+        'HTML_formatter': OfferReportHTMLFormatter,
40
+    }
41
+
42
+    def generate(self):
43
+        discounts = OrderDiscount._default_manager.filter(
44
+            order__date_placed__gte=self.start_date,
45
+            order__date_placed__lt=self.end_date
46
+        )
47
+        offer_discounts = {}
48
+        for discount in discounts:
49
+            if discount.offer_id not in offer_discounts:
50
+                try:
51
+                    offer = ConditionalOffer._default_manager.get(id=discount.offer_id)
52
+                except ConditionalOffer.DoesNotExist:
53
+                    continue
54
+                offer_discounts[discount.offer_id] = {
55
+                    'offer': offer,
56
+                    'total_discount': D('0.00')
57
+                }
58
+            offer_discounts[discount.offer_id]['total_discount'] += discount.amount
59
+
60
+        return self.formatter.generate_response(offer_discounts.values())

+ 37
- 12
oscar/apps/order/reports.py 查看文件

@@ -4,20 +4,15 @@ from django.db.models import get_model
4 4
 
5 5
 from oscar.core.loading import get_class
6 6
 ReportGenerator = get_class('dashboard.reports.reports', 'ReportGenerator')
7
+ReportCSVFormatter = get_class('dashboard.reports.reports', 'ReportCSVFormatter')
8
+ReportHTMLFormatter = get_class('dashboard.reports.reports', 'ReportHTMLFormatter')
7 9
 Order = get_model('order', 'Order')
8 10
 
9 11
 
10
-class OrderReportGenerator(ReportGenerator):
11
-    
12
+class OrderReportCSVFormatter(ReportCSVFormatter):
12 13
     filename_template = 'orders-%s-to-%s.csv'
13
-    code = 'order_report'
14
-    description = "Orders placed"
15
-    
16
-    def generate(self, response):
17
-        orders = Order._default_manager.filter(
18
-            date_placed__gte=self.start_date
19
-        ).filter(date_placed__lt=self.end_date)
20
-        
14
+
15
+    def generate_csv(self, response, orders):
21 16
         writer = csv.writer(response)
22 17
         header_row = ['Order number',
23 18
                       'User',
@@ -30,6 +25,36 @@ class OrderReportGenerator(ReportGenerator):
30 25
                    order.total_incl_tax,
31 26
                    self.format_datetime(order.date_placed)]
32 27
             writer.writerow(row)
33
-            
28
+
29
+    def filename(self, **kwargs):
30
+        return self.filename_template % (kwargs['start_date'], kwargs['end_date'])
31
+
32
+
33
+class OrderReportHTMLFormatter(ReportHTMLFormatter):
34
+    filename_template = 'dashboard/reports/partials/order_report.html'
35
+
36
+
37
+class OrderReportGenerator(ReportGenerator):
38
+
39
+    code = 'order_report'
40
+    description = "Orders placed"
41
+
42
+    formatters = {
43
+        'CSV_formatter': OrderReportCSVFormatter,
44
+        'HTML_formatter': OrderReportHTMLFormatter,
45
+    }
46
+
47
+    def generate(self):
48
+        orders = Order._default_manager.filter(
49
+            date_placed__gte=self.start_date
50
+        ).filter(date_placed__lt=self.end_date)
51
+
52
+        additional_data = {
53
+            'start_date': self.start_date,
54
+            'end_date': self.end_date
55
+        }
56
+
57
+        return self.formatter.generate_response(orders, **additional_data)
58
+
34 59
     def is_available_to(self, user):
35
-        return user.is_staff and user.has_perm('order.can_view')
60
+        return user.is_staff and user.has_perm('order.can_view')

+ 29
- 11
oscar/apps/voucher/reports.py 查看文件

@@ -3,17 +3,17 @@ import csv
3 3
 from django.db.models import get_model
4 4
 
5 5
 from oscar.core.loading import get_class
6
+
6 7
 ReportGenerator = get_class('dashboard.reports.reports', 'ReportGenerator')
8
+ReportCSVFormatter = get_class('dashboard.reports.reports', 'ReportCSVFormatter')
9
+ReportHTMLFormatter = get_class('dashboard.reports.reports', 'ReportHTMLFormatter')
7 10
 Voucher = get_model('voucher', 'Voucher')
8 11
 
9 12
 
10
-class VoucherReportGenerator(ReportGenerator):
11
-    
13
+class VoucherReportCSVFormatter(ReportCSVFormatter):
12 14
     filename_template = 'voucher-performance.csv'
13
-    code = 'vouchers'
14
-    description = 'Voucher performance'
15
-    
16
-    def generate(self, response):
15
+
16
+    def generate_csv(self, response, vouchers):
17 17
         writer = csv.writer(response)
18 18
         header_row = ['Voucher code',
19 19
                       'Added to a basket',
@@ -21,11 +21,29 @@ class VoucherReportGenerator(ReportGenerator):
21 21
                       'Total discount',
22 22
                      ]
23 23
         writer.writerow(header_row)
24
-        
25
-        vouchers = Voucher._default_manager.all()
24
+
26 25
         for voucher in vouchers:
27
-            row = [voucher.code, voucher.num_basket_additions, voucher.num_orders, voucher.total_discount]
26
+            row = [voucher.code,
27
+                   voucher.num_basket_additions,
28
+                   voucher.num_orders,
29
+                   voucher.total_discount]
28 30
             writer.writerow(row)
29 31
 
30
-    def filename(self):
31
-        return self.filename_template
32
+
33
+class VoucherReportHTMLFormatter(ReportHTMLFormatter):
34
+    filename_template = 'dashboard/reports/partials/voucher_report.html'
35
+
36
+
37
+class VoucherReportGenerator(ReportGenerator):
38
+
39
+    code = 'vouchers'
40
+    description = 'Voucher performance'
41
+
42
+    formatters = {
43
+        'CSV_formatter': VoucherReportCSVFormatter,
44
+        'HTML_formatter': VoucherReportHTMLFormatter
45
+    }
46
+
47
+    def generate(self):
48
+        vouchers = Voucher._default_manager.all()
49
+        return self.formatter.generate_response(vouchers)

+ 3
- 1
oscar/templates/catalogue/browse.html 查看文件

@@ -49,7 +49,9 @@
49 49
     {% if category.description %}
50 50
         <div class="row-fluid">
51 51
             <div class="span9"><p>{{ category.description }}</p></div>
52
-            <div class="span3"><img src="{{ category.image.url }}" alt="{{ summary }}" /></div>
52
+			{% if category.image %}
53
+				<div class="span3"><img src="{{ category.image.url }}" alt="{{ summary }}" /></div>
54
+			{% endif %}
53 55
         </div>
54 56
     {% endif %}
55 57
 

+ 3
- 0
oscar/templates/dashboard/catalogue/product_update.html 查看文件

@@ -34,6 +34,9 @@ Update product | {{ block.super }}
34 34
         <div class="sub-header">
35 35
             <h3 class="app-ico ico_expand icon">Product information</h3>
36 36
         </div>
37
+		<div class="control-group fields-full">
38
+			Product class: <strong>{{ product.product_class }}</strong>
39
+		</div>
37 40
         {% for field in form %}
38 41
             {% if forloop.counter > 4 %}
39 42
                 <div class="form-inline pull-left">

+ 8
- 0
oscar/templates/dashboard/reports/index.html 查看文件

@@ -31,5 +31,13 @@ Reports | {{ block.super }}
31 31
     </div>
32 32
 </form>
33 33
 
34
+{% if description %}
35
+<div class="sub-header">
36
+	<h3>{{ description }}</h3>
37
+</div>
38
+{% endif %}
39
+{% block report %}
40
+{% endblock %}
41
+
34 42
 {% endblock dashboard_content %}
35 43
 

+ 24
- 0
oscar/templates/dashboard/reports/partials/offer_report.html 查看文件

@@ -0,0 +1,24 @@
1
+{% extends 'dashboard/reports/index.html' %}
2
+{% load currency_filters %}
3
+
4
+{% block report %}
5
+{% if objects %}
6
+	<table class="table table-striped table-bordered">
7
+		<tr>
8
+			<th>Offer</th>
9
+			<th>Total discount</th>
10
+		</tr>
11
+		{% for offer_discount in objects %}
12
+		<tr>
13
+			<td>{{ offer_discount.offer.name }}</td>
14
+			<td>{{ offer_discount.total_discount|currency }}</td>
15
+		</tr>
16
+		{% endfor %}
17
+	</table>
18
+    {% if page_obj %}
19
+        {% include "catalogue/partials/pagination.html" %}
20
+    {% endif %}
21
+{% else %}
22
+	<p>No results found.</p>
23
+{% endif %}
24
+{% endblock %}

+ 30
- 0
oscar/templates/dashboard/reports/partials/open_basket_report.html 查看文件

@@ -0,0 +1,30 @@
1
+{% extends 'dashboard/reports/index.html' %}
2
+{% load currency_filters %}
3
+
4
+{% block report %}
5
+<table class="table table-striped table-bordered">
6
+    <tr>
7
+        <th>Email</th>
8
+        <th>Name</th>
9
+        <th>Num lines</th>
10
+        <th>Num items</th>
11
+        <th>Value</th>
12
+        <th>Date of creation</th>
13
+        <th>Time since creation</th>
14
+    </tr>
15
+    {% for basket in objects %}
16
+    <tr>
17
+        <td>{{ basket.owner.email }}</td>
18
+        <td>{{ basket.owner.get_full_name|default:"-" }}</td>
19
+        <td>{{ basket.num_lines }}</td>
20
+        <td>{{ basket.num_items }}</td>
21
+        <td>{{ basket.total_incl_tax|currency }}</td>
22
+        <td>{{ basket.date_created }}</td>
23
+        <td>{{ basket.time_since_creation }}</td>
24
+    </tr>
25
+    {% endfor %}
26
+</table>
27
+    {% if page_obj %}
28
+        {% include "catalogue/partials/pagination.html" %}
29
+    {% endif %}
30
+{% endblock %}

+ 32
- 0
oscar/templates/dashboard/reports/partials/order_report.html 查看文件

@@ -0,0 +1,32 @@
1
+{% extends 'dashboard/reports/index.html' %}
2
+{% load currency_filters %}
3
+
4
+{% block report %}
5
+{% if objects %}
6
+	<table class="table table-striped table-bordered">
7
+		<tr>
8
+			<th>Order number</th>
9
+			<th>User</th>
10
+			<th>Total incl. tax</th>
11
+			<th>Date placed</th>
12
+			<th></th>
13
+		</tr>
14
+		{% for order in objects %}
15
+		<tr>
16
+			<td>{{ order.number }}</td>
17
+			<td>{{ order.user }}</td>
18
+			<td>{{ order.total_incl_tax|currency }}</td>
19
+			<td>{{ order.date_placed }}</td>
20
+			<td>
21
+				<a class="btn btn-info" href="{% url dashboard:order-detail order.number %}">View</a>
22
+			</td>
23
+		</tr>
24
+		{% endfor %}
25
+	</table>
26
+    {% if page_obj %}
27
+        {% include "catalogue/partials/pagination.html" %}
28
+    {% endif %}
29
+{% else %}
30
+	<p>No results found.</p>
31
+{% endif %}
32
+{% endblock %}

+ 24
- 0
oscar/templates/dashboard/reports/partials/product_report.html 查看文件

@@ -0,0 +1,24 @@
1
+{% extends 'dashboard/reports/index.html' %}
2
+{% load currency_filters %}
3
+
4
+{% block report %}
5
+<table class="table table-striped table-bordered">
6
+    <tr>
7
+        <th>Product</th>
8
+        <th>Views</th>
9
+        <th>Basket additions</th>
10
+        <th>Purchases</th>
11
+    </tr>
12
+    {% for product in objects %}
13
+    <tr>
14
+        <td>{{ product.product }}</td>
15
+        <td>{{ product.num_views }}</td>
16
+        <td>{{ product.num_basket_additions }}</td>
17
+        <td>{{ product.num_purchases }}</td>
18
+    </tr>
19
+    {% endfor %}
20
+</table>
21
+    {% if page_obj %}
22
+        {% include "catalogue/partials/pagination.html" %}
23
+    {% endif %}
24
+{% endblock %}

+ 30
- 0
oscar/templates/dashboard/reports/partials/submitted_basket_report.html 查看文件

@@ -0,0 +1,30 @@
1
+{% extends 'dashboard/reports/index.html' %}
2
+{% load currency_filters %}
3
+
4
+{% block report %}
5
+<table class="table table-striped table-bordered">
6
+    <tr>
7
+        <th>Email</th>
8
+        <th>Name</th>
9
+        <th>Num lines</th>
10
+        <th>Num items</th>
11
+        <th>Value</th>
12
+        <th>Date created</th>
13
+        <th>Time between creation and submission</th>
14
+    </tr>
15
+    {% for basket in objects %}
16
+    <tr>
17
+        <td>{{ basket.owner.email }}</td>
18
+        <td>{{ basket.owner.get_full_name|default:"-" }}</td>
19
+        <td>{{ basket.num_lines }}</td>
20
+        <td>{{ basket.num_items }}</td>
21
+        <td>{{ basket.total_incl_tax|currency }}</td>
22
+        <td>{{ basket.date_created }}</td>
23
+        <td>{{ basket.time_before_submit }}</td>
24
+    </tr>
25
+    {% endfor %}
26
+</table>
27
+    {% if page_obj %}
28
+        {% include "catalogue/partials/pagination.html" %}
29
+    {% endif %}
30
+{% endblock %}

+ 36
- 0
oscar/templates/dashboard/reports/partials/user_report.html 查看文件

@@ -0,0 +1,36 @@
1
+{% extends 'dashboard/reports/index.html' %}
2
+{% load currency_filters %}
3
+
4
+{% block report %}
5
+<table class="table table-striped table-bordered">
6
+    <tr>
7
+        <th>Email</th>
8
+        <th>Name</th>
9
+        <th>Date registered</th>
10
+        <th>Product views</th>
11
+        <th>Basket additions</th>
12
+        <th>Orders</th>
13
+        <th>Order lines</th>
14
+        <th>Order items</th>
15
+        <th>Total spent</th>
16
+        <th>Date of last order</th>
17
+    </tr>
18
+    {% for user in objects %}
19
+    <tr>
20
+        <td>{{ user.user.email }}</td>
21
+        <td>{{ user.user.get_full_name|default:"-" }}</td>
22
+        <td>{{ user.user.date_joined }}</td>
23
+        <td>{{ user.num_product_views }}</td>
24
+        <td>{{ user.num_basket_additions }}</td>
25
+        <td>{{ user.num_orders }}</td>
26
+        <td>{{ user.num_order_lines }}</td>
27
+        <td>{{ user.num_order_items }}</td>
28
+        <td>{{ user.total_spent|currency }}</td>
29
+        <td>{{ user.date_last_order|default:"-" }}</td>
30
+    </tr>
31
+    {% endfor %}
32
+</table>
33
+    {% if page_obj %}
34
+        {% include "catalogue/partials/pagination.html" %}
35
+    {% endif %}
36
+{% endblock %}

+ 24
- 0
oscar/templates/dashboard/reports/partials/voucher_report.html 查看文件

@@ -0,0 +1,24 @@
1
+{% extends 'dashboard/reports/index.html' %}
2
+{% load currency_filters %}
3
+
4
+{% block report %}
5
+<table class="table table-striped table-bordered">
6
+    <tr>
7
+        <th>Voucher code</th>
8
+        <th>Added to a basket</th>
9
+        <th>Used in an order</th>
10
+        <th>Total discount</th>
11
+    </tr>
12
+    {% for voucher in objects %}
13
+    <tr>
14
+        <td>{{ voucher.code }}</td>
15
+        <td>{{ voucher.num_basket_additions }}</td>
16
+        <td>{{ voucher.num_orders }}</td>
17
+        <td>{{ voucher.total_discount|currency }}</td>
18
+    </tr>
19
+    {% endfor %}
20
+</table>
21
+    {% if page_obj %}
22
+        {% include "catalogue/partials/pagination.html" %}
23
+    {% endif %}
24
+{% endblock %}

+ 0
- 4
release.sh 查看文件

@@ -5,10 +5,6 @@ git tag | grep $RELEASE_NUM > /dev/null && \
5 5
 	echo "New version number required ($RELEASE_NUM already used)" && exit 1
6 6
 
7 7
 # Push to PyPi
8
-:xa
9
-
10
-
11
-
12 8
 ./setup.py sdist upload
13 9
 
14 10
 # Tag in Git

Loading…
取消
儲存