Django-SHOP documentation¶
Software Architecture¶
The django-SHOP framework is, as its name implies, a framework and not a software which runs out of the box. Instead, an e-commerce site built upon django-SHOP, always consists of this framework, a bunch of other Django apps and the merchant’s own implementation. While this may seem more complicate than a ready-to-use solution, it gives the programmer enormous advantages during the implementation:
Not everything can be “explained” to a software system using graphical user interfaces. After reaching a certain point of complexity, it normally is easier to pour those requirements into executable code, rather than to expect yet another set of configuration buttons.
When evaluating django-SHOP with other e-commerce solutions, I therefore suggest to do the following litmus test:
Consider a product which shall be sold world-wide. Depending on the country’s origin of the request, use the native language and the local currency. Due to export restrictions, some products can not be sold everywhere. Moreover, in some countries the value added tax is part of the product’s price, and must be stated separately on the invoice, while in other countries, products are advertised using net prices, and tax is added later on the invoice.
Instead of looking for software which can handle such a complex requirement, rethink about writing your own plugins, able to handle this. With the django, REST and django-SHOP frameworks, this normally is possible in a few dozen lines of clearly legible Python code. Compare this to solutions, which claim to handle such complex requirements. They normally are shipped containing huge amounts of features, which very few merchants ever require, but which bloat the overall system complexity, making such a piece of software expensive to maintain.
Design Decisions¶
Single Source of Truth¶
A fundamental aspect of good software design is to follow the principle of “Don’t repeat yourself”, often denoted as DRY. In django-SHOP we aim for a single source of truth, wherever possible.
For instance have a look at the shop.models.address.BaseShippingAddress
. Whenever we
add, change or remove a field, the ORM mapper of Django gets notified and with
./manage.py makemigrations
followed by ./manage.py migrate
our database scheme is updated.
But even the input fields of our address form adopt to all changes in our address model. Even the
client side form field validation adopts to every change in our address model. As we can see, here
our single source of truth is the address model.
Feature Completeness¶
A merchant who wants to implement a unique feature for his e-commerce site, must never have to touch the code of the framework. Aiming for ubiquity means, that no matter how challenging a feature is, it must be possible to be implemented into the merchant’s own implementation, rather than by patching the framework itself.
Otherwise this framework contains a bug - not just a missing feature! I’m sure some merchants will come up with really weird ideas, I never have thought of. If the django-SHOP framework inhibits to add a feature, then feel free to create a bug report. The claim “feature completeness” for a framework is the analogue to the term “Turing completeness” for programming languages.
Consider that on many sites, a merchant’s requirement is patched into existing code. This means that every time a new version of the e-commerce software is released, that patch must be repeatedly adopted. This can become rather dangerous when security flaws in that software must be closed immediately. DjangoSHOP instead is designed, so that the merchant’s implementation and third party plugins have to subclass its models and to override its templates accordingly.
Minimalism¶
In a nutshell, django-SHOP offers this set of basic functionalities, to keep the framework simple and stupid (KISS) without reinventing the wheel:
- A catalog to display product lists and detail views.
- Some methods to add products to the cart.
- A way to remove items from the cart or change their quantities.
- A set of classes to modify the cart’s totals.
- A collection of forms, where customers can add personal, shipping and payment information.
- A way to perform the purchase: this converts the cart into an order.
- A list view where customers can lookup their previously performed orders
- A backend tool which helps to track the state of orders.
All functionality required to build a real e-commerce site, sits on top of this. Computing taxes for instance, can vary a lot among different legislations and thus is not part of the framework. The same applies for vouchers, rebates, delivery costs, etc.
These are the parts, which must be fine tuned by the merchant. They can be rather complicate to implement and are best implemented by separate plugins.
Separation of Concern¶
Compared to other e-commerce solutions, the django-SHOP framework has a rather small footprint in terms of code lines, database tables and classes. This does not mean, that its functionality is somehow limited. Instead, the merchant’s own implementation can become rather large. This is because django-SHOP implies dependencies to many third party Django apps.
Having layered systems gives us programmers many advantages:
- We don’t have to reinvent the wheel for every required feature.
- Since those dependencies are used in other applications, they normally are tested quite well.
- No danger to create circular dependencies, as found often in big libraries and stand alone applications.
- Better overview for newcomers, which part of the system is responsible for what.
- Easier to replace one component against another one.
Fortunately Django gives us all the tools to stitch those dependencies together. If for instance we would use one of the many PHP-based e-commerce system, we’d have to stay inside their modest collection for third party apps, or reinvent the wheel. This often is a limiting factor compared to the huge ecosystems arround Django.
Inversion of Control¶
Wherever possible, django-SHOP tries to delegate the responsibility for taking decision to the merchant’s implementation of the site. Let explain this by a small example: When the customer adds a product to the cart, django-SHOP consults the implementation of the product to determine whether the given item is already part of the cart or not. This allows the merchant’s implementation to fine tune its product variants.
Core System¶
Generally, the shop system can be seen in three different phases:
The shopping phase¶
From a customers perspective, this is where we look around at different products, presumably in different categories. We denote this as the catalog list- and catalog detail views. Here we browse, search and filter for products. In one of the list views, we edit the quantity of the products to be added to our shopping cart.
Each time a product is added, the cart is updated which in turn run the so named “Cart Modifiers”. Cart modifiers sum up the line totals, add taxes, rebates and shipping costs to compute the final total. The Cart Modifiers are also during the checkout phase (see below), since the chosen shipping method and destination, as well as the payment method may modify the final total.
The checkout process¶
Here the customer must be able to refine the cart’ content: Change the quantity of an item, or remove that item completely from the cart.
During the checkout process, the customer must enter his addresses and payment informations. These settings may also influence the cart’s total.
The final step during checkout is the purchase operation. This is where the cart’s content is converted into an order object and emptied afterwards.
The fulfillment phase¶
It is now the merchants’s turn to take further steps. Depending on the order status, certain actions must be performed immediately or the order must be kept in the current state until some external events happen. This could be a payment receivement, or that an ordered item arrived in stock. While setting up a django-SHOP project, the allowed status transitions for the fulfillment phase can be plugged together, giving the merchant the possibility to programmatically define his order workflows.
Plugins¶
Django SHOP defines 5 types of different plugins:
- Product models
- Cart modifiers
- Payment backends
- Shipping backends
- Order workflow modules
They may be added as a third party django-SHOP plugin, or integrated into the merchant’s implementation.
Unique Features of django-SHOP¶
django-SHOP requires to describe your products instead of prescribing prefabricated models¶
Products can vary wildly, and modeling them is not always trivial. Some products are salable in pieces, while others are continues. Trying to define a set of product models, capable for describing all such scenarios is impossible – describe your product by customizing the model and not vice versa.
E-commerce solutions, claiming to be plug-and-play, normally use one of these (anti-)patterns¶
Either, they offer a field for every possible variation, or they use the Entity-Attribute-Value pattern to add meta-data for each of your models. This at a first glance seems to be easy. But both approaches are unwieldy and have serious drawbacks. They both apply a different “physical schema” – the way data is stored, rather than a “logical schema” – the way users and applications require that data. As soon as you have to combine your e-commerce solution with some Enterprise-Resource-Planning software, additional back-and-forward conversion routines have to be added.
In django-SHOP, the physical representation of a product corresponds to its logical¶
django-SHOP‘s approach to this problem is to have minimal set of models. These abstract models are stubs provided to subclass the physical models. Hence the logical representation of the product conforms to their physical one. Moreover, it is even possible to represent various types of products by subclassing polymorphically from an abstract base model. Thanks to the Django framework, modeling the logical representation for a set of products, together with an administration backend, becomes almost effortless.
Django-SHOP is multilingual¶
Products offered in various regions, normally require attributes in different natural languages. For such a set of products, these attributes can be easily modelled using translatable fields. This lets you seamlessly built a multilingual e-commerce site.
Django-SHOP supports multiple currencies¶
Django-SHOP is shipped with a set of currency types, bringing their own money arithmetic. This
adds an additional layer of security, because one can not accidentally sum up different currencies.
These money types always know how to represent themselves in different local environments, prefixing
their amount with the correct currency symbol. They also offer the special amount “no price”
(represented by –
), which behaves like zero but is handy for gratuitous items.
Django-SHOP directly plugs into django-CMS¶
Product detail pages may use all templatetags from djangoCMS, such as the {% placeholder ... %}
,
the {% static_placeholder ... %}
, or other CMS tags.
Django-SHOP does not presuppose categories to organize product list views. Instead django-CMS pages can be specialized to handle product lists via a CMS app. This allows the merchant to organize products into categories, using the existing page hierarchy from the CMS. It also allows to offer single products from a CMS page, without requiring any category.
Django-SHOP is based on REST¶
- Django-SHOP uses the Django REST framework and hence does not require any Django View
- Every view is based on REST interfaces.
- Infinite scrolling and paginated listings use the same template.
- Views for cart, checkout etc. can be inserted into exiting pages.
- This means that one can navigate through products, add them to the cart, modify the cart, register himself as new customer (or proceed as guest), add his shipping information, pay via Stripe and view his past orders. Other Payment Service Providers can be added in a pluggable manner.
Every page in the shop: product-list, product-detail, cart, checkout-page, orders-list, order-detail etc. is part of the CMS and can be edited through the plugin editor. The communication between the client and these pages is done exclusively through REST. This has the nice side-effect, that the merchants shop implementation does not require any Django-View.
Django-SHOP is shipped with individual components for each task. These plugins then can be placed into any CMS placeholder using the plugin editor. Each of these plugins is shipped with their own overridable template, which can also be used as a stand-alone template outside of a CMS placeholder. Templates for bigger tasks, such as the Cart-View are granular, so that the HTML can be overridden partially.
Authentication is done through auth-rest, which allows to authenticate against a bunch of social networks, such as Google+, Facebook, GitHub, etc in a pluggable manner.
Moreover, the checkout process is based on a configurable finite state machine, which means that a merchant can adopt the shops workflow to the way he is used to work offline.
Client code is built using Bootstrap-3.3 and AngularJS-1.3. jQuery is required only for the backends administration interface. All browser components have been implemented as AngularJS directives, so that they can be reused between projects. For instance, my current merchant implementation does not have a single line of customized JavaScript.
This makes it very easy, even for non-programmers, to implement a shop. A merchant only has to adopt his product models, optionally the cart and order models, and override the templates.
Upgrading¶
If you are upgrading from an earlier version, please be sure to read the upgrade instructions.
Tutorial¶
This tutorial shows how to setup a working e-commerce site with django-SHOP using the given dependencies. The code required to setup this demo can be found in the example/myshop folder.
Tutorial¶
This tutorial is aimed at people new to django SHOP but already familiar with Django. If you aren’t yet, reading their excellent Django Tutorial is highly recommended.
Introduction¶
Django-SHOP is shipped with 6 demos: The Commodity Product Model, The Internationalized Commodity Product Model, The Smart Card Product Model, The Internationalized Smart Card Model, The Polymorphic Product Model and The Internationalized Polymorphic Product Model.
You may install them manually and populate the database yourself. The recommended way however, is to install them manually, and Populate the Database using Fixtures.
If you have a Docker runtime on your host, an even quicker approach is to Start with a prepared Docker Image.
Prepare the Installation¶
To run the examples shown in this tutorial, you must install django-shop from GitHub, since the pip-installable from PyPI only contains the framework, but not the files required for the demos. Before proceeding, please make sure virtualenv is installed on your system, otherwise you would pollute your Python site-packages folder.
Because we are using the current Github master for this tutorial, you must also use the documentation for the current Github master. If you are reading this document on Read The Docs, please look for the version selector (usually at the bottom of the screen) and select “latest”.
Also ensure that these packages are installed using the favorite package manager of your operating system:
- Python 2.7, 3.4 or 3.5
- Redis: http://redis.io/
- SQLite: https://www.sqlite.org/
- Node Package Manager: https://www.npmjs.com/
- Optionally Elasticsearch version 1.7.3 (not later)
$ mkdir Tutorial; cd Tutorial
$ virtualenv -p $(which python3) shoptutorial
$ source shoptutorial/bin/activate
(shoptutorial)$ pip install -U pip setuptools
(shoptutorial)$ git clone --depth 1 https://github.com/awesto/django-shop
(shoptutorial)$ cd django-shop
(shoptutorial)$ pip install 'Django<1.11'
(shoptutorial)$ pip install -r requirements/common.txt
(shoptutorial)$ pip install --no-deps -e .
(shoptutorial)$ cd example
(shoptutorial)$ npm install
These statements will setup an environment that runs one of the demo shops out of the box.
Note
We recommend that you use Python 3, but if you stuck with Python-2.7, please note that
you have to replace requirements/common.txt
with requirements/py2.txt
.
If you want to populate the database with your own categories, products and pages, proceed as described below. Otherwise, or if impatient, you may Quickstart with Docker using prepared CMS page layouts, products and media files.
Create a database for the demo¶
Finally we must create a database to run our example project:
(shoptutorial)$ cd django-shop/example
(shoptutorial)$ export DJANGO_SHOP_TUTORIAL=commodity DJANGO_DEBUG=1
(shoptutorial)$ ./manage.py migrate
(shoptutorial)$ ./manage.py createsuperuser
Email address: admin@example.org
Username: admin
Password:
Password (again):
Superuser created successfully.
(shoptutorial)$ ./manage.py runserver
If Elasticsearch is installed and running, optionally build the search index:
(shoptutorial)$ ./manage.py rebuild_index
Finally point a browser onto http://localhost:8000/ and log in as the superuser you just created.
Presumably you are somehow disappointed now, because there is only one empty page served by the CMS. There are no pages for the catalog, the cart, the checkout and the orders. In django-SHOP this is by intention, because we prefer to arrange our web components inside the CMS rather than using hard coded templates.
For gaining a first impression of django-SHOP, this can be quite annoying. Therefore it is recommended to Populate the Database using Fixtures.
Populate the Database using Fixtures¶
If you want to use the demo as a starting point for your own project, then instead of creating the database manually and Adding pages to the CMS, it presumably is quicker to start with a prepared fixture using the following steps:
(shoptutorial)$ cd django-shop/example
(shoptutorial)$ export DJANGO_SHOP_TUTORIAL=i18n_polymorphic DJANGO_DEBUG=1
(shoptutorial)$ ./manage.py initialize_shop_demo
(shoptutorial)$ ./manage.py runserver
Point a browser onto http://localhost:8000/admin/ and sign in as user admin with password secret. It may take a few minutes until the database is ready.
This runs the demo for The Internationalized Polymorphic Product Model. By changing the environment
variable DJANGO_SHOP_TUTORIAL
to commodity
, i18n_commodity
, smartcard
,
i18n_smartcard
or polymorphic
, you can examine one of the other prepared examples.
Afterwards re-run ./manage.py initialize_shop_demo
for each of them.
All demos can be started independently from each other, but you are encouraged to begin with the
commodity
example, and then proceed to the more complicate ones.
Note
The first time, django-SHOP renders a page, images must be thumbnailed and cropped. This is an expensive operation which runs only once. Therefore please be patient, when loading pages for the first time.
Adding pages to the CMS¶
In django-SHOP, every page, can be rendered by the CMS. Therefore, unless you need a special landing page, start immediately with the Catalog’s List View of your products. Change into the Django Admin backend, choose the section
Start > django CMS > Pages
and add a Page. As its Title choose “Smart Cards”. Then change into the Advanced Settings at the bottom of the page. In this editor window, locate the field Application and select Products List. Then save the page and click on View on site.
Now change into Structure mode and locate the placeholder named Main Content. Add a plugin from section Bootstrap named Row. Below that Row add a Column with a width of 12 units. Finally, below the last Column add a plugin from section Shop named Catalog List View.
Now we have a working catalog list view, but since we havn’t added any products to the database yet, we won’t see any items on our page.
The Commodity Product Model¶
The commodity
demo shows how to setup a monolingual shop, with a generic product, named
Commodity. The product model shop.models.defauls.commodity.Commodity
is part of the
django-SHOP framework. It is intended for shops where the merchant does not want to create a
customized product model, but rather prefers to create the product’s detail views using common CMS
functionality.
A Commodity model contains only the following attributes:
- The name of the product.
- The product code.
- The slug (a short label used as the last bit in the URLs).
- The product’s unit price.
- One sample image to be shown in the catalog’s list view.
- A caption to be shown in the catalog’s list view.
The detail view for each product must however be styled individually using a DjangoCMS placeholder together with the plugin system provided by djangocms-cascade. This gives the merchant all the flexibility to style each product’s detail page individually and without having to create a special HTML template. Into the provided placeholder we then can add as many text fields as we want. Additionally we can use image galleries, carousels, different backgrounds, tab sets, etc.
One plugin which should always be present is the Add Product to Cart plugin as found in section Shop, otherwise a customer wouldn’t be able to add that product to the cart and thus purchasing anything.
Using the Commodity product model only makes sense, if the merchant does not require special product attributes and normally is only suitable for shops with up to a dozen articles. Otherwise, creating a reusable HTML template is probably less effort, than filling the placeholder for each product’s detail page individually.
The Internationalized Commodity Product Model¶
The i18n_commodity
demo shows how to setup a shop, with the same generic product as in the
previous example, but with these attributes translatable into multiple natural languages:
- The name of the product.
- The slug.
- A caption to be shown in the catalog’s list view.
All other product attributes from our Commodity model are shared across all languages.
Using this internationalized configuration, requires to additionally install django-parler.
The Smart Card Product Model¶
The smartcard
demo shows how to setup a shop with a model, created explicitly to describe a
certain type of product. Smart Cards have many different attributes such as their card type, the
manufacturer, storage capacity and the maximum transfer speed. Here it’s the merchant’s
responsibility to create the database model according to the physical properties of the product.
The class myshop.models.smartcard.SmartCard
therefore is not part of the shop’s framework,
but rather in the merchant’s implementation as found in our example.
Creating a customized product model is only a few lines of declarative Python code. Additionally we have to create a Django template using HTML. It however keeps us from having to build a page using plugins, for each product item we want to offer. It also helps us to structure our products using attributes rather than describing them in a free form.
The Internationalized Smart Card Model¶
The i18n_smartcard
demo is a variation of the above example, with a few attributes translated
into multiple languages, namely caption
and description
. The product name of a Smart Card
is international anyways and doesn’t require to be translated into different langauges. Hence we
don’t require a translatable field for the product name and its slug.
The Polymorphic Product Model¶
The polymorphic
demo is a combination from all of the examples from above. Here we declare a
base product model using the class myshop.models.polymorphic_.Product
. We also declare
common fields available in all of our different product types. These fields act as the smallest
common denominator for the views where we want to display summary information about our products,
independently of their characteristics. This generally is the product’s name, a thumbnailed image,
the price and often a caption.
List views showing a summary information about our products are the Cart View, the Order Detail View and eventually the Catalog List View.
The model classes for Smart Card, Smart Phone and a variation of Commodity then inherits from this base product class. These models additionally can declare attributes required to describe the physical properties of each product type. Since they vary, we also have to create special templates for the detail views of each of them. Smart Phones for instance allow product variations, therefore we must adopt the template for adding the product to the cart.
The Internationalized Polymorphic Product Model¶
The i18n_polymorphic
demo is a variation of the above example, with a few attributes translated
into multiple languages, namely caption
and description
. This sample implementation does not
use translated slugs, although it would be possible.
Use one of the demos as a starting point for your project¶
Depending on the needs of your e-commerce site, the easiest approach to start with your implementation of django-SHOP, is to use one of the six demo samples from above:
- If you only require a free form product description, go ahead with the
commodity
ori18n_commodity
sample. - If you need a shop with one specific product type, then go ahead with the
smartcard
ori18n_smartcard
sample. Rename the product model to whatever makes sense and add additional fields to describe the properties of your model. - If you need a shop with different product types, then go ahead with the
polymorphic
ori18n_polymorphic
sample. Specify the common fields in the product’s base class and add additional fields to describe the properties each of your product model.
It also is suggested to reuse the current structure of CMS pages and placeholders from the given samples. Having a working implementation, it is much easier to gradually modify it, until you reach a final goal, rather than starting with an empty site from scratch.
Quickstart with Docker¶
To get a first impression of the django-SHOP demos, please use one of the prepared Docker images. If not already available on your workstation, first install the Docker runtime environment and start the local Docker machine.
Start with a prepared Docker Image¶
To run a fully configured django-SHOP container on your local machine:
docker run --name demo-shop-i18n_polymorphic --env DJANGO_SHOP_TUTORIAL=i18n_polymorphic -p 9001:9001 awesto/django-shop-demo:latest
This image is rather large (~2 GB), therefore it may take some time to download.
Locate the IP address of the running container using docker-machine ip default
. Then point
a browser onto this address using port 9001, for instance http://192.168.99.100:9001/ (the IP
address may vary depending on your Docker machine settings) or http://localhost:9001/ if running on
Linux.
After the container started, it may take a few minutes until the database is ready. The first time a page is loaded, this also takes additional time because all images must be thumbnailed. Therefore, if you stop the running container with
docker stop demo-shop-i18n_polymorphic
instead of re-running the supplied Docker image, it is recommended to restart the just created container with:
$ docker start demo-shop-i18n_polymorphic
To access the administration backed, navigate to http://192.168.99.100:9001/admin/ and sign in as user “admin” with password “secret”. If you navigate to any page of the shop, you may switch into live edit mode and change the content of the various pages, including the product’s details pages.
Try out the other examples¶
By changing the environment variable DJANGO_SHOP_TUTORIAL
to commodity
, i18n_commodity
,
smartcard
, i18n_smartcard
, polymorphic
or i18n_polymorphic
, you can examine one of
the other prepared examples. Afterwards re-create the container using the same Docker image:
docker run --name demo-shop-commodity --env DJANGO_SHOP_TUTORIAL=commodity -p 9001:9001 awesto/demo-shop:latest
Troubleshooting¶
To access a running Docker container from outside, attach to it using:
docker exec -ti demo-shop-i18n_polymorphic /bin/bash
[root@example]# ps fax
If you don’t want to interfere with the running container, you may create a “throw-away” container
and access files through the shared volume named /web
. Here you can read the logs and change
some basic settings. If you modify the timestamp of /web/workdir/myshop.ini
uWSGI restarts
the Django server. To access this shared volume, start a throw away container with:
docker run --rm -ti --volumes-from demo-shop-commodity demo-shop /bin/bash
[root@example]# cd /web/logs
[root@example]# less shop.log
Modeling a simple product¶
As a simple example, this tutorial uses Smart Cards as its first product. As emphasized earlier, django-SHOP is not shipped with ready to use product models. Instead the merchant must declare these models based on the products properties. Lets have a look ar a model describing a typical Smart Card:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | from djangocms_text_ckeditor.fields import HTMLField
from shop.money.fields import MoneyField
from shop.models.product import BaseProduct, BaseProductManager, CMSPageReferenceMixin
from shop.models.defaults.mapping import ProductPage, ProductImage
from shop.models.defaults.order import Order
from ..manufacturer import Manufacturer
class SmartCard(CMSPageReferenceMixin, BaseProduct):
# common product fields
product_name = models.CharField(
_("Product Name"),
max_length=255
)
slug = models.SlugField(_("Slug"))
unit_price = MoneyField(
_("Unit price"),
decimal_places=3,
help_text=_("Net price for this product"),
)
caption = HTMLField(
_("Caption"),
configuration='CKEDITOR_SETTINGS_CAPTION',
help_text=_("Short description used in the catalog's list view of products."),
)
images = models.ManyToManyField(
'filer.Image',
through=ProductImage,
)
|
Here our model SmartCard
inherits directly from BaseProduct
, which is a stub class, hence
the most common fields, such as product_name
, slug
and unit_price
must be added to our
product here. Later on we will see why these fields, even though required by each product, can not
be part of our abstract model BaseProduct
.
Additionally a smart card has some product specific properties:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | # product properties
manufacturer = models.ForeignKey(
Manufacturer,
verbose_name=_("Manufacturer")
)
card_type = models.CharField(
_("Card Type"),
choices=(2 * ('{}{}'.format(s, t),)
for t in ('SD', 'SDXC', 'SDHC', 'SDHC II') for s in ('', 'micro ')),
max_length=15,
)
speed = models.CharField(
_("Transfer Speed"),
choices=((str(s), "{} MB/s".format(s))
for s in (4, 20, 30, 40, 48, 80, 95, 280)),
max_length=8,
)
product_code = models.CharField(
_("Product code"),
max_length=255,
unique=True,
)
storage = models.PositiveIntegerField(
_("Storage Capacity"),
help_text=_("Storage capacity in GB"),
)
|
these class attributes depend heavily on the data sheet of the product to sell.
Finally we also want to position our products into categories and sort them:
1 2 3 4 5 6 7 8 9 10 11 12 | # controlling the catalog
order = models.PositiveIntegerField(
_("Sort by"),
db_index=True,
)
cms_pages = models.ManyToManyField(
'cms.Page',
through=ProductPage,
help_text=_("Choose list view this product shall appear on."),
)
|
The field order
is used to keep track on the sequence of our products while rendering a list
view.
The field cms_pages
specifies on which pages of the CMS a product shall appear.
Note
If categories do not require to keep any technical properties, it often is completely sufficient to use CMS pages as their surrogates.
Finally images
is another many-to-many relation, allowing to associate none, one or more images
to a product.
Both fields cms_pages
and images
must use the through parameter. This is because we have
two many-to-many mapping tables which are part of the merchant’s project rather than the
django-SHOP application. The first of those mapping tables has foreign keys onto the models
cms.Page
and myshop.SmartCard
. The second table has foreign keys onto the models
filer.Image
and myshop.SmartCard
again. Since the model myshop.SmartCard
has been
declared by the merchant himself, he also is responsible for managing those many-to-many mapping
tables.
Additionally each product model requires these attributes:
- A model field or property method named
product_name
: It must returns the product’s name in its natural language. - A method
get_price(request)
: Returns the product price. This can depend on the given region, which is available through the request object. - A method
get_absolute_url()
: Returns the canonical URL of a product. - The
object
attribute must be of typeBaseProductManager
or derived from thereof.
These product model attributes are optional, but highly recommended:
- A model field or property method named
product_code
: It shall returns a language independent product code or article number. - A property method
sample_image
: It shall returns a sample image for the given product.
Add Model myshop.SmartCard
to Django Admin¶
For reasons just explained, it is the responsibility of the project to manage the many-to-many
relations between its CMS pages and the images on one side, and the product on the other side.
Therefore we can’t use the built-in admin widget FilteredSelectMultiple
for these relations.
Instead django-SHOP is shipped with a special mixin class CMSPageAsCategoryMixin
, which
handles the relation between CMS pages and the product. This however implies that the field used
to specify this relation is named cms_pages
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | from adminsortable2.admin import SortableAdminMixin
from shop.admin.product import CMSPageAsCategoryMixin, ProductImageInline
from myshop.models import SmartCard
@admin.register(SmartCard)
class SmartCardAdmin(SortableAdminMixin, CMSPageAsCategoryMixin, admin.ModelAdmin):
fieldsets = (
(None, {
'fields': ('product_name', 'slug', 'product_code', 'unit_price', 'active', 'description',),
}),
(_("Properties"), {
'fields': ('manufacturer', 'storage', 'card_type', 'speed',)
}),
)
inlines = (ProductImageInline,)
prepopulated_fields = {'slug': ('product_name',)}
list_display = ('product_name', 'product_code', 'unit_price', 'active',)
search_fields = ('product_name',)
|
For images, the admin class must use a special inline class named ProductImageInline
. This is
because the merchant might want to arrange the order of the images and therefore a simple
SelectMultiple
widget won’t do this job here.
Extend our simple product to support other natural languages by Modeling a Multilingual Product.
Modeling a Multilingual Product¶
Let’s extend our previous SmartCard
model to internationalize our shop site. Normally the name
of a Smart Card model is international anyway, say “Ultra Plus micro SDXC”, so it probably won’t
make much sense to use a translatable field here. The model attribute which certainly makes sense
to be translated into different languages, is the description
field.
Run the Multilingual Demo¶
To test this example, set the shell environment variable export DJANGO_SHOP_TUTORIAL=i18n_smartcard
,
then apply the modified models to the database schema:
./manage.py migrate myshop
Alternatively recreate the database as explained in Create a database for the demo.
Afterwards start the demo server:
./manage.py runserver
The Multilingal Product Model¶
DjangoSHOP uses the library django-parler for model translations. We therefore shall rewrite our model as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
from djangocms_text_ckeditor.fields import HTMLField
from parler.managers import TranslatableManager, TranslatableQuerySet
from parler.models import TranslatableModelMixin, TranslatedFieldsModel
from parler.fields import TranslatedField
from polymorphic.query import PolymorphicQuerySet
from shop.money.fields import MoneyField
from shop.models.product import BaseProductManager, BaseProduct, CMSPageReferenceMixin
from shop.models.defaults.order import Order
from ..manufacturer import Manufacturer
__all__ = ['SmartCard', 'Order']
class ProductManager(BaseProductManager, TranslatableManager):
queryset_class = ProductQuerySet
def get_queryset(self):
card_type = models.CharField(_("Card Type"), choices=CARD_TYPE, max_length=15)
SPEED = ((str(s), "{} MB/s".format(s)) for s in (4, 20, 30, 40, 48, 80, 95, 280))
def get_price(self, request):
return self.unit_price
class SmartCardTranslation(TranslatedFieldsModel):
master = models.ForeignKey(SmartCard, related_name='translations', null=True)
caption = HTMLField(verbose_name=_("Caption"),
configuration='CKEDITOR_SETTINGS_CAPTION',
help_text=_("Short description used in the catalog's list view of products."))
description = HTMLField(verbose_name=_("Description"),
configuration='CKEDITOR_SETTINGS_DESCRIPTION',
help_text=_("Full description used in the catalog's detail view of Smart Cards."))
class Meta:
unique_together = [('language_code', 'master')]
|
In comparison to the simple Smart Card model, the field description
can now accept text in
different languages.
In order to work properly, a model with translations requires an additional model manager and a
table storing the translated fields. Accessing an instance of this model behaves exactly the same
as an untranslated model. Therefore it can be used as a drop-in replacement for our simple
SmartCard
model.
Translatable model in Django Admin¶
The admin requires only a small change. Its class must additionally inherit from
TranslatableAdmin
. This adds a tab for each configured language to the top of the detail
editor. Therefore it is recommended to group all multilingual fields into one fieldset to emphasize
that these fields are translatable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from adminsortable2.admin import SortableAdminMixin
from parler.admin import TranslatableAdmin
from shop.admin.product import CMSPageAsCategoryMixin, ProductImageInline
from myshop.models import SmartCard
@admin.register(SmartCard)
class SmartCardAdmin(SortableAdminMixin, TranslatableAdmin,
CMSPageAsCategoryMixin, admin.ModelAdmin):
fieldsets = (
(None, {
'fields': ('product_name', 'slug', 'product_code', 'unit_price', 'active',),
}),
(_("Translatable Fields"), {
'fields': ('caption', 'description',)
}),
(_("Properties"), {
'fields': ('manufacturer', 'storage', 'card_type',)
}),
)
inlines = (ProductImageInline,)
prepopulated_fields = {'slug': ('product_name',)}
list_display = ('product_name', 'product_code', 'unit_price', 'active',)
search_fields = ('product_name',)
|
Extend our discrete product type, to polymorphic models which are able to support many different product types: Products with Different Properties.
Products with Different Properties¶
In the previous examples we have seen that we can model our products according to their physical properties, but what if we want to sell another type of a product with different properties. This is where polymorphism enters the scene.
Run the Polymorphic Demo¶
To test this example, set the shell environment variable export DJANGO_SHOP_TUTORIAL=polymorphic
,
then recreate the database as explained in Create a database for the demo and start the demo
server:
./manage.py runserver
The Polymorphic Product Model¶
If in addition to Smart Cards we also want to sell Smart Phones, we must declare a new model.
Here instead of duplicating all the common fields, we unify them into a common base class named
Product
. Then that base class shall be extended to become either our known model SmartCard
or a new model SmartPhone
.
To enable polymorphic models in django-SHOP, we require the application django-polymorphic.
Here our models for Smart Cards or Smart Phones will be split up into a generic part and a
specialized part. The generic part goes into our new Product
model, whereas the specialized
parts remain in their models.
You should already start to think about the layout of the list views. Only attributes in model
Product
will be available for list views displaying Smart Phones side by side with Smart Cards.
First we must create a special Model Manager which unifies the query methods for translatable
and polymorphic models:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
from shop.models.product import BaseProductManager, BaseProduct, CMSPageReferenceMixin
from shop.models.defaults.mapping import ProductPage, ProductImage
from ..manufacturer import Manufacturer
@python_2_unicode_compatible
class Product(CMSPageReferenceMixin, BaseProduct):
Base class to describe a polymorphic product. Here we declare common fields available in all of
our different product types. These common fields are also used to build up the view displaying
a list of all products.
product_name = models.CharField(
_("Product Name"),
max_length=255,
slug = models.SlugField(
_("Slug"),
unique=True,
)
)
|
The next step is to identify which model attributes qualify for being part of our Product model. Unfortunately, there is no silver bullet for this problem and that’s one of the reason why django-SHOP is shipped without any prepared model for it. If we want to sell both Smart Cards and Smart Phones, then this Product model may do its jobs:
1 2 3 4 5 6 7 8 9 | caption = HTMLField(
verbose_name=_("Caption"),
blank=True,
null=True,
configuration='CKEDITOR_SETTINGS_CAPTION',
help_text=_("Short description used in the catalog's list view of products."),
)
# common product properties
|
Model for Smart Card¶
The model used to store translated fields is the same as in our last example. The new model for Smart Cards now inherits from Product:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | from django.db import models
from django.utils.translation import ugettext_lazy as _
from djangocms_text_ckeditor.fields import HTMLField
from shop.money.fields import MoneyField
from .product import Product, BaseProductManager
class SmartCard(Product):
# common product fields
unit_price = MoneyField(
_("Unit price"),
decimal_places=3,
help_text=_("Net price for this product"),
)
# product properties
CARD_TYPE = (2 * ('{}{}'.format(s, t),)
for t in ('SD', 'SDXC', 'SDHC', 'SDHC II') for s in ('', 'micro '))
card_type = models.CharField(
|
Model for Smart Phone¶
The product model for Smart Phones is intentionally a little bit more complicated. Not only does
it have a few more attributes, but Smart Phones can be sold with different specifications of
internal storage. The latter influences the price and the product code. This is also the reason why
we didn’t move the model fields unit_price
and products_code
into our base class
Product
, although every product in our shop requires them.
When presenting Smart Phones in our list views, we want to focus on different models, but not on each flavor, ie. its internal storage. Therefore customers will have to differentiate between the concrete Smart Phone variations, whenever they add them to their cart, but not when viewing them in the catalog list. For a customer, it would be very boring to scroll through lists with many similar products, which only differentiate by a few variations.
This means that for some Smart Phone models, there is be more than one Add to cart button.
When modeling, we therefore require two different classes, one for the Smart Phone model and one for each Smart Phone variation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
from shop.money import Money, MoneyMaker
from djangocms_text_ckeditor.fields import HTMLField
_("Name"),
max_length=50,
)
def __str__(self):
return self.name
class SmartPhoneModel(Product):
"""
A generic smart phone model, which must be concretized by a model `SmartPhone` - see below.
"""
BATTERY_TYPES = (
(1, "Lithium Polymer (Li-Poly)"),
(2, "Lithium Ion (Li-Ion)"),
)
WIFI_CONNECTIVITY = (
(1, "802.11 b/g/n"),
)
BLUETOOTH_CONNECTIVITY = (
(1, "Bluetooth 4.0"),
(2, "Bluetooth 3.0"),
(3, "Bluetooth 2.1"),
)
battery_type = models.PositiveSmallIntegerField(
|
Here the method get_price()
can only return the minimum, average or maximum price for our
product. In this situation, most merchants extol the prices as: Price starting at € 99.50.
The concrete Smart Phone then is modeled as:
1 2 3 4 5 6 7 8 9 10 | )
weight = models.DecimalField(
_("Weight"),
max_digits=5,
decimal_places=1,
help_text=_("Weight in gram"),
)
screen_size = models.DecimalField(
|
To proceed with purchasing, customers need some Cart and Checkout pages.
Model for a generic Commodity¶
For demo purposes, this polymorphic example adds another kind of Product model, a generic Commodity.
Here instead of adding every possible attribute of our product to the model, we try to remain as
generic as possible, and instead use a PlaceholderField
as provided by djangoCMS.
from cms.models.fields import PlaceholderField
from myshop.models.product import Product
class Commodity(Product):
# other product fields
placeholder = PlaceholderField("Commodity Details")
This allows us to add any arbitrary information to our product’s detail page. The only requirement for this to work is, that the rendering template adds a templatetag to render this placeholder.
Since the django-SHOP framework looks in the folder catalog
for a template named after its
product class, adding this HTML snippet should do the job:
{% extends "myshop/pages/default.html" %}
{% load cms_tags %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>{% render_model product "product_name" %}</h1>
{% render_placeholder product.placeholder %}
</div>
</div>
</div>
This detail template extends the default template of our site. Apart from the product’s name (which has added as a convenience), this view remains empty when first viewed. In Edit mode, double clicking on the heading containing the product name, opens the detail editor for our commodity.
After switching into Structure mode, a placeholder named Commodity Details
appears. Here we
can add as many Cascade plugins as we want, by subdividing our placeholder into rows, columns,
images, text blocks, etc. It allows us to edit the detail view of our commodity in whatever layout
we like. The drawback using this approach is, that it can lead to inconsistent design and is much
more labor intensive, than just editing the product’s attributes together with their appropriate
templates.
Configure the Placeholder¶
Since we use this placeholder inside a hard-coded Bootstrap column, we must provide a hint to Cascade about the widths of that column. This has to be done in the settings of the project:
CMS_PLACEHOLDER_CONF = {
...
'Commodity Details': {
'plugins': ['BootstrapRowPlugin', 'TextPlugin', 'ImagePlugin', 'PicturePlugin'],
'text_only_plugins': ['TextLinkPlugin'],
'parent_classes': {'BootstrapRowPlugin': []},
'require_parent': False,
'glossary': {
'breakpoints': ['xs', 'sm', 'md', 'lg'],
'container_max_widths': {'xs': 750, 'sm': 750, 'md': 970, 'lg': 1170},
'fluid': False,
'media_queries': {
'xs': ['(max-width: 768px)'],
'sm': ['(min-width: 768px)', '(max-width: 992px)'],
'md': ['(min-width: 992px)', '(max-width: 1200px)'],
'lg': ['(min-width: 1200px)'],
},
}
},
...
}
This placeholder configuration emulates the Bootstrap column as declared by
<div class="col-xs-12">
.
Catalog Views¶
Now that we know how to create product models and how to administer them, lets have a look on how to route them to our views.
When editing the CMS page used for the products list view, open Advanced Settings and choose Products List from the select box labeled Application.
Then choose a template with at least one placeholder. Click onto View on site to change into front-end editing mode. Locate the main placeholder and add a Row followed by a Column plugin from the section Bootstrap. Below that column add a Catalog List Views plugin from section Shop. Then publish the page, it should not display any products yet.
Add products to the category¶
Open the detail view of a product in Django’s administration backend. Locate the many-to-many select box labeled Categories > Cms pages. Select the pages where each product shall appear on.
On reloading the list view, the assigned products now shall be visible. Assure that they have been set to be active, otherwise they won’t show up.
If you nest categories, products assigned to children will be also be visible on their parents pages.
Product Model Serializers¶
We already learned how to write model classes and model managers, so what are serializers for?
In django-SHOP the response views do not distinguish whether the product’s information shall be rendered as HTML or transferred via JSON. This gives us the ability to use the same business logic for web browsers rendering static HTML, single page web applications communicating via AJAX or native shopping applications for your mobile devices. This btw. is one of the great benefits when working with RESTful API’s and thanks to the djangorestframework we don’t even have to write any Django Views anymore.
For instance, try to open the list- or the detail view of any of the products available in the
shop. Then in the browsers URL input field append ?format=api
or ?format=json
to the URL.
This will render the pure product information, but without embedding it into HTML.
The REST API view is very handy while developing. If you want to hide this on your production
system , then in your settingy.py remove 'rest_framework.renderers.BrowsableAPIRenderer'
from
REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES']
.
In the shop’s catalog, we need some functionality to render a list view for all products and we need a detail view to render each product type. The django-SHOP framework supplies two such serializers:
Serialize for the Products List View¶
For each product we want to display in a list view, we need a serializer which converts the content of the most important fields of a product. Normally these are the Id, the name and price, the URL onto the detail view, a short description and a sample image.
The django-SHOP framework does not know which of those fields have to be serialized, therefore it requires some help from the programmer:
1 2 3 4 5 6 7 8 | from shop.serializers.bases import BaseProductSummarySerializer
from myshop.models.polymorphic.product import Product
class ProductSummarySerializer(BaseProductSummarySerializer):
class Meta:
model = Product
fields = ('id', 'product_name', 'product_url',
'product_type', 'product_model', 'price')
|
All these fields can be extracted directly from the product model with the exception of the sample
image. This is because we yet do not know the final dimensions of the image inside its HTML element
such as <img src="...">
, and we certainly want to resize it using PIL/Pillow before it is
delivered. An easy way to solve this problem is to use the SerializerMethodField
. Simply extend
the above class to:
1 2 3 4 5 6 7 | from rest_framework.serializers import SerializerMethodField
class ProductSummarySerializer(BaseProductSummarySerializer):
media = SerializerMethodField()
def get_media(self, product):
return self.render_html(product, 'media')
|
As you might expect, render_html
assigns a HTML snippet to the field media
in the serialized
representation of our product. This method uses a template to render the HTML. The name of this
template is constructed using the following rules:
- Look for a folder named according to the project’s name, ie.
settings.SHOP_APP_LABEL
in lower case. If no such folder can be found, then use the folder namedshop
. - Search for a subfolder named
products
. - Search for a template named “label-product_type-postfix.html”. These three subfieds are
determined using the following rule:
* label: the component of the shop, for instance
catalog
,cart
,order
. * product_type: the class name in lower case of the product’s Django model, for instancesmartcard
,smartphone
or if no such template can be found, justproduct
. * postfix: This is an arbitrary name passed in by the rendering function. As in the example above, this is the stringmedia
.
Note
It might seem “un-restful” to render HTML snippets by a REST serializer and deliver them
via JSON to the client. However, we somehow must re-size the images assigned to our product to
fit into the layout of our list view. The easiest way to do this in a configurable manner is
to use the easythumbnails library and its templatetag {% thumbnail product.sample_image ... %}
.
The template to render the media snippet could look like:
{% load i18n thumbnail djng_tags %}
{% thumbnail product.sample_image 100x100 crop as thumb %}
<img src="{{ thumb.url }}" width="{{ thumb.width }}" height="{{ thumb.height }}">
The template of the products list view then may contain a list iteration such as:
{% for product in data.results %}
<div class="shop-list-item">
<a href="{{ product.product_url }}">
<h4>{{ product.product_name }}</h4>
{{ product.media }}
<strong>{% trans "Price" %}: {{ product.price }}</strong>
</a>
</div>
{% endfor %}
The tag {{ product.media }}
inserts the HTML snippet as prepared by the serializer from above.
A serializer may add more than one SerializerMethodField
. This can be useful, if the list view
shall render different product types using different snippet templates.
Serialize for the Product’s Detail View¶
The serializer for the Product’s Detail View is very similar to its List View serializer. In the example as shown below, we even reverse the field listing by explicitly excluding the fields we’re not interested in, rather than naming the fields we want to include. This for the product’s detail view makes sense, since we want to expose every possible detail.
1 2 3 4 5 6 | from shop.serializers.bases import BaseProductDetailSerializer
class ProductDetailSerializer(BaseProductDetailSerializer):
class Meta:
model = Product
exclude = ('active',)
|
The AddToCartSerializer
¶
Rather than using the detail serializer, the business logic for adding a product to the cart has been moved into a specialized serializer. This is because django-SHOP can not presuppose that products are added to the cart only from within the detail view[#add2cart]_. We also need a way to add more than one product variant to the cart from each products detail page.
For this purpose django-SHOP is shipped with an AddToCartSerializer
. It can be overridden
for special product requirements, but for a standard application it just should work out of the box.
Assure that the context for rendering a product contains the key product
referring to the
product object. The ProductDetailSerializer
does this by default. Then add
{% include "shop/catalog/product-add2cart.html" %}
to an appropriate location in the template which renders the product detail view.
The now included add-to-cart template contains a form with some input fields and a few AngularJS
directives, which communicate with the endpoint connected to the AddToCartSerializer
. It
updates the subtotal whenever the customer changes the quantity and displays a nice popup window,
whenever an item is added to the cart. Of course, that template can be extended with arbitrary HTML.
These Angular JS directives require some JavaScript code which is located in the file
shop/js/catalog.js
; it is referenced automatically when using the above template include
statement.
[1] | Specially in business-to-business sites, this usually is done in the list views. |
Connect the Serializers with the View classes¶
Now that we declared the serializers for the product’s list- and detail view, the final step is to access them through a CMS page. Remember, since we’ve chosen to use CMS pages as categories, we had to set a special djangoCMS apphook:
1 2 3 4 5 6 7 8 | from cms.app_base import CMSApp
from cms.apphook_pool import apphook_pool
class ProductsListApp(CMSApp):
name = _("Products List")
urls = ['myshop.urls.products']
apphook_pool.register(ProductsListApp)
|
This apphook points onto a list of boilerplate code containing these urlpattern:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from django.conf.urls import url
from rest_framework.settings import api_settings
from shop.rest.filters import CMSPagesFilterBackend
from shop.rest.serializers import AddToCartSerializer
from shop.views.catalog import (CMSPageProductListView,
ProductRetrieveView, AddToCartView)
urlpatterns = [
url(r'^$', CMSPageProductListView.as_view(
serializer_class=ProductSummarySerializer,
)),
url(r'^(?P<slug>[\w-]+)$', ProductRetrieveView.as_view(
serializer_class=ProductDetailSerializer
)),
url(r'^(?P<slug>[\w-]+)/add-to-cart', AddToCartView.as_view()),
]
|
These URL patterns connect the product serializers with the catalog views in order to assign them
an endpoint. Additional note: The filter class CMSPagesFilterBackend
is used to restrict
products to specific CMS pages, hence it can be regarded as the product categoriser.
Cart and Checkout¶
In django-SHOP, the cart and checkout view follow the same idea as all other pages – they are managed by the CMS. Change into the Django admin backend and look for the CMS page tree. A good position for adding a page is the root level, but then assure that in Advanced Setting the checkbox Soft root is set.
The checkout can be combined with the cart on the same page or moved on a separate page. Its best position normally is just below the Cart page.
The Checkout pages presumably are the most complicated page to setup. Therefore no generic receipt can be presented here. Instead some CMS plugins will be listed here. They can be useful to compose a complete checkout page. In the reference section it is shown in detail how to create a Cart and Checkout view, but for this tutorial the best way to proceed is to have a look in the prepared demo project for the Cart and Checkout pages.
A list of plugins specific to django-SHOP can be found in the reference section. They include a cart editor, a static cart renderer, forms to enter the customers names, addresses, payment- and shipping methods, credit card numbers and some more.
Other useful plugins can be found in the Django application djangocms-cascade.
Scaffolding¶
Depending on who is allowed to buy products, keep in mind that visitors must declare themselves whether they want to buy as guest or as registered user. This means that we first must distinguish between visitor and recognized customer. The simplest way to do this is to use the Segmentation if- and else-plugin. A recognized customer shall be able to proceed directly to the purchasing page. A visitor first must declare himself, this could be handled with a collections of plugins, such as:
in structure mode. This collection of plugins then will be rendered as:
Please note that the Authentication plugins Login & Reset, Register User and Continue as guest must reload the current page. This is because during these steps a new session-id is assigned, which requires a full page reload.
After reloading the page, the customer is considered as “recognized”. Since there are a few forms to be filled, this example uses a Process Bar plugin, which emulates a few sub-pages, which then can be filled out by the customer step-by-step.
A fragment of this collection of plugins then will be rendered as:
Reference¶
Reference to classes and concepts used in django-SHOP
Customer Model¶
Most web applications distinguish logged in users explicitly from the anonymous site visitor, which is regarded as a non-existing user, and hence does not reference a session- or database entity. The Django framework, in this respect, is no exception.
This pattern is fine for web-sites, which run a Content Management System or a Blog, where only an elected group of staff users shall be permitted to access. This approach also works for web-services, such as social networks or Intranet applications, where visitors have to authenticate right on from the beginning of their session.
But when running an e-commerce site, this use-pattern has serious drawbacks. Normally, a visitor starts to look for interesting products, hopefully adding a few of them to their cart. Then on the way to the checkout, they decide whether to create a user account, use an existing one or continue as guest. Here’s where things get complicated.
First of all, for non-authenticated site visitors, the cart does not belong to anybody. But each cart must be associated with its current site visitor, hence the generic anonymous user object is not appropriate for this purpose. Unfortunately the Django framework does not offer an explicit but anonymous user object based on the assigned session-Id.
Secondly, at the latest when the cart is converted into an order, but the visitor wants to continue as guest (thus remaining anonymous), that order object must refer to an user object in the database. These kind of users would be regarded as fakes: Unable to log in, reset their password, etc. The only information which must be stored for such a faked user, is their email address otherwise they couldn’t be informed, whenever the state of their order changes.
Django does not explicitly allow such user objects in its database models. But by using the boolean
flag is_active
, we can fool an application to interpret such a guest visitor as a faked
anonymous user.
However, since such an approach is unportable across all Django based applications, django-SHOP
introduces a new database model – the Customer
model, which extends the existing User
model.
Properties of the Customer Model¶
The Customer
model has a 1:1 relation to the existing User
model, which means that for each
customer, there always exists one and only one user object. This approach allows us to do a few
things:
The built-in User
model can be swapped out and replaced against another implementation. Such an
alternative implementation has a small limitation. It must inherit from
django.contrib.auth.models.AbstractBaseUser
and from django.contrib.auth.models.PermissionMixin
.
It also must define all the fields which are available in the default model as found in
django.contrib.auth.models.User
.
By setting the flag is_active = False
, we can create guests inside Django’s User
model.
Guests can not sign in, they can not reset their password, and hence can be considered as
“materialized” anonymous users.
Having guests with an entry in the database, gives us another advantage: By using the session key
of the site visitor as the user object’s username
, it is possible to establish a link between a
User
object in the database with an otherwise anonymous visitor. This further allows the
Cart
and the Order
models always refer to the User
model, since they don’t have to care
about whether a certain user authenticated himself or not. It also keeps the workflow simple,
whenever an anonymous user decides to register and authenticate himself in the future.
Adding the Customer model to our application¶
As almost all models in django-SHOP, the Customer
model itself, uses the
Deferred Model Pattern. This means that the Django project is responsible for
materializing that model and additionally allows the merchant to add arbitrary fields to his
Customer
model. Sound choices are a phone number, birth date, a boolean to signal whether the
customer shall receive newsletters, his rebate status, etc.
The simplest way is to materialize the given Customer
class as found in our default and
convenience models:
from shop.models.defaults.customer import Customer
or, if we need extra fields, then instead of the above, we create a customized Customer
model:
from shop.models.customer import BaseCustomer
class Customer(BaseCustomer):
birth_date = models.DateField("Date of Birth")
# other customer related fields
Configure the Middleware¶
A Customer object is created automatically with each visitor accessing the site. Whenever Django’s
internal AuthenticationMiddleware adds an AnonymousUser
to the request object, then the
django-SHOP‘s CustomerMiddleware adds a VisitingCustomer
to the request object as well.
Neither the AnonymousUser
nor the VisitingCustomer
are stored inside the database.
Whenever the AuthenticationMiddleware adds an instantiated User
to the request object,
then the django-SHOP‘s CustomerMiddleware adds an instantiated Customer
to the request object
as well. If no associated Customer
exists yet, the CustomerMiddleware creates one.
Therefore add the CustomerMiddleware after the AuthenticationMiddleware in the project’s
settings.py
:
MIDDLEWARE_CLASSES = (
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
'shop.middleware.CustomerMiddleware',
...
)
Configure the Context Processors¶
Additionally, some templates may need to access the customer object through the RequestContext
.
Therefore, add this context processor to the settings.py
of the project.
TEMPLATE_CONTEXT_PROCESSORS = (
...
'shop.context_processors.customer',
...
)
Implementation Details¶
The Customer
model has a non-nullable one-to-one relation to the User
model. Therefore each
customer is associated with exactly one user. For instance, accessing the hashed password can be
achieved through customer.user.password
. Some common fields and methods from the User
model,
such as first_name
, last_name
, email
, is_anonymous()
and is_authenticated()
are
accessible directly, when working with a Customer
object. Saving an instance of type
Customer
also invokes the save()
method from the associated User
model.
The other direction – accessing the Customer
model from a User
– does not always work.
Accessing an attribute that way fails if the corresponding customer object is missing, ie. if there
is no reverse relation from a Customer
pointing onto the given User
object.
>>> from django.contrib.auth import get_user_model
>>> user = get_user_model().create(username='bobo')
>>> print user.customer.salutation
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "django/db/models/fields/related.py", line 206, in __get__
self.related.get_accessor_name()))
DoesNotExist: User has no customer.
This can happen for User
objects added manually or by other Django applications.
During database queries, django-SHOP always performs and INNER JOIN between the customer and the
user table. Therefore it performs better to query the User
via the Customer
object, rather
than vice versa.
Anonymous Users and Visiting Customers¶
Most requests to our site will be of anonymous nature. They will not send a cookie containing a
session-Id to the client, and the server will not allocate a session bucket. The middleware adds
a VisitingCustomer
object associated with an AnonymousUser
object to the request. These
two objects are not stored inside the database.
Whenever such an anonymous user/visiting customer adds his first item to the cart, django-SHOP
instantiates a user object in the database and associates it with a customer object. Such a
customer is considered as “unregistered” and invoking customer.is_authenticated()
will return
False
; here its associated User
model is inactive and has an unusable password.
Guests and Registered Customers¶
On the way to the checkout, a customer must declare himself, whether to continue as guest, to
sign in using an existing account or to register himself with a new account. In the former case
(customer wishes to proceed as guest), the User` object remains as it is: Inactive and with an
unusable password. In the second case, the visitor signs in using Django's default authentication
backends. Here the cart's content is merged with the already existing cart of that user object.
In the latter case (customer registers himself), the user object is recycled and becomes an active
Django ``User
object, with a password and an email address.
Obviate Criticism¶
Some may argue that adding unregistered and guest customers to the user table is an anti-pattern or hack. So, what are the alternatives?
We could keep the cart of anonymous customers in the session store. This was the procedure used until django-SHOP version 0.2. It however required to keep two different models of the cart, one session based and one relational. Not very practical, specially if the cart model should be overridable by the merchant’s own implementation.
We could associate each cart models with a session id. This would require an additional field which would be NULL for authenticated customers. While possible in theory, it would require a lot of code which distinguishes between anonymous and authenticated customers. Since the aim of this software is to remain simple, this idea was dismissed.
We could keep the primary key of each cart in the session associated with an anonymous user/customer. But this would it make very hard to find expired carts, because we would have to iterate over all carts and for each cart we would have to iterate over all sessions to check if the primary keys matches. Remember, there is no such thing as an OUTER JOIN between sessions and database tables.
We could create a customer object which is independent of the user. Hence instead of having a
OneToOneField(AUTH_USER_MODEL)
in model Customer
, we’d have this 1:1 relation with a
nullable foreign key. This would require an additional field to store the session id in the customer
model. It also would require an additional email field, if we wanted guest customers to remain
anonymous users – what they actually are, since they can’t sign in. Apart from field duplication,
this approach would also require some code to distinguish between unrecognized, guest and
registered customers. In addition to that, the administration backend would require two
distinguished views, one for the customer model and one for the user model.
Authenticating against the Email Address¶
Nowadays it is quite common, to use the email address for authenticating, rather than an explicit
account identifier. This in Django is not possible without replacing the built-in User
model.
Since for an e-commerce site this authentication variant is rather important, django-SHOP is
shipped with an optional drop-in replacement for the built-in User
model.
This User
model is almost identical to the existing User
model as found in
django.contrib.auth.models.py
. The difference is that it uses the field email
rather than
username
for looking up the credentials. To activate this alternative User model, add that
alternative authentication app to the project’s settings.py
:
INSTALLED_APPS = (
'django.contrib.auth',
'email_auth',
...
)
AUTH_USER_MODEL = 'email_auth.User'
Note
This alternative User
model uses the same database table as the Django authentication
would, namely auth_user
. It is even field-compatible with the built-in model and hence
can be added later to an existing Django project.
Caveat when using this alternative User model¶
The savvy reader may have noticed that in email_auth.models.User
, the email field is not
declared as unique. This by the way causes Django to complain during startup with:
WARNINGS:
email_auth.User: (auth.W004) 'User.email' is named as the 'USERNAME_FIELD', but it is not unique.
HINT: Ensure that your authentication backend(s) can handle non-unique usernames.
This warning can be silenced by adding SILENCED_SYSTEM_CHECKS = ['auth.W004']
to the project’s
settings.py
.
The reason for this is twofold:
First, Django’s default User
model has no unique constraint on the email field, so
email_auth
remains more compatible.
Second, the uniqueness is only required for users which actually can sign in. Guest users on the
other hand can not sign in, but they may return someday. By having a unique email field, the
Django application email_auth
would lock them out and guests would be allowed to buy only once,
but not a second time – something we certainly do not want!
Therefore django-SHOP offers two configurable options:
- Customers can declare themselves as guests, each time they buy something. This is the default setting, but causes to have non-unique email addresses in the database.
- Customer can declare themselves as guests the first time they buys something. If someday they
return to the site a buy a second time, they will be recognized as returning customer and must
use a form to reset their password. This configuration is activated by setting
SHOP_GUEST_IS_ACTIVE_USER = True
. It further allows us, to set a unique constraint on the email field.
Note
The email field from Django’s built-in User
model has a max-length of 75 characters.
This is enough for most use-cases but violates RFC-5321, which requires 254 characters. The
alternative implementation uses the correct max-length.
Administration of Users and Customers¶
By keeping the Customer
and the User
model tight together, it is possible to reuse the
Django’s administration backend for both of them. All we have to do is to import and register the
customer backend inside the project’s admin.py
:
from django.contrib import admin
from shop.admin.customer import CustomerProxy, CustomerAdmin
admin.site.register(CustomerProxy, CustomerAdmin)
This administration backend recycles the built-in django.contrib.auth.admin.UserAdmin
, and
enriches it by adding the Customer model as a StackedInlineAdmin
on top of the detail page.
By doing so, we can edit the customer and user fields on the same page.
Summary for Customer to User mapping¶
This table summarizes to possible mappings between a Django User
model [1] and the Shop’s
Customer
model:
Shop’s Customer Model | Django’s User Model | Active Session |
---|---|---|
VisitingCustomer object |
AnonymousUser object |
No |
Unrecognized Customer |
Inactive User object with unusable
password |
Yes, but not logged in |
Customer recognized as guest [2] |
Inactive User with valid email
address and unusable password |
Yes, but not logged in |
Customer recognized as guest [3] |
Active User with valid email address
and unusable, but resetable password |
Yes, but not logged in |
Registered Customer |
Active User with valid email
address, known password, optional
salutation, first- and last names,
and more |
Yes, logged in using Django’s authentication backend |
[1] | or any alternative User model, as set by AUTH_USER_MODEL . |
[2] | if setting SHOP_GUEST_IS_ACTIVE_USER = False (the default). |
[3] | if setting SHOP_GUEST_IS_ACTIVE_USER = True . |
Manage Customers¶
Django-SHOP is shipped with a special management command which informs the merchant about the state of customers. In the project’s folder, invoke on the command line:
./manage.py shop_customers
Customers in this shop: total=20482, anonymous=17418, expired=10111, active=1068, guests=1997, registered=1067, staff=5.
Read these numbers as:
- Anonymous customers are those which added at least one item to the cart, but never proceeded to checkout.
- Expired customers are the subset of the anonymous customers, whose session already expired.
- The difference between guest and registered customers is explained in the above table.
Delete expired customers¶
By invoking on the command line:
./manage.py shop_customers --delete-expired
This removes all anonymous/unregistered customers and their associated user entities from the database, whose session expired. This command may be used to reduce the database storage requirements.
Deferred Model Pattern¶
Until django-SHOP version 0.2, there were abstract and concrete and models: BaseProduct
and
Product
, BaseCart
and Cart
, BaseCartItem
and CartItem
, BaseOrder
and
Order
and finally, BaseOrderItem
and OrderItem
.
The concrete models were stored in shop.models
, whereas abstract models were stored in
shop.models_bases
. This was quite confusing and made it difficult to find the right model
definition whenever one had to access the definition of one of the models.
Additionally, if someone wanted to subclass a model, he had to use a configuration directive, say
PRODUCT_MODEL
, ORDER_MODEL
, ORDER_MODEL_ITEM
from the projects settings.py
.
This made configuration quite complicate and causes other drawbacks:
- Unless all models have been overridden, the native ones appeared in the administration backend below the category Shop, while the customized ones appeared under the given project’s name. To merchants, this inconsistency in the backend was quite difficult to explain.
- In the past, mixing subclassed with native models caused many issues with circular dependencies.
Therefore in django-SHOP, since version 0.9 all concrete models, Product
, Order
,
OrderItem
, Cart
, CartItem
have been removed. These model definitions now all are
abstract and named BaseProduct
, BaseOrder
, BaseOrderItem
, etc. They all have been moved
into the folder shop/models/
, because that’s the location a programmer expects them.
Materializing Models¶
Materializing such an abstract base model, means to create a concrete model with an associated database table. This model creation is performed in the concrete project implementing the shop; it must be done for each base model in the shop software.
For instance, materialize the cart by using this code snippet inside our own shop’s
models/shopmodels.py
files:
from shop.models import cart
class Cart(cart.BaseCart):
my_extra_field = ...
class Meta:
app_label = 'my_shop'
class CartItem(cart.BaseCartItem):
other_field = ...
class Meta:
app_label = 'my_shop'
Of course, we can add as many extra model fields to this concrete cart model, as we wish.
All shop models, now are managed through our project instance. This means that the models
Cart, Order, etc. are now managed by the common database migrations tools, such as
./manage.py makemigration my_shop
and ./manage.py migrate my_shop
. This
also means that these models, in the Django admin backend, are visible under my_shop.
Use the default Models¶
Often we don’t need extra fields, hence the abstract shop base model is enough. Then,
materializing the models can be done using some convenience classes as found in
shop/models/defaults
. We can simply import them into models.py
or models/__init__.py
in
our own shop project:
from shop.models.defaults.cart import Cart # nopyflakes
from shop.models.defaults.cart_item import CartItem # nopyflakes
Note
The comment nopyflakes
has been added to suppress warnings, since these classes
arern’t used anywhere in models.py
.
All the configuration settings from django-SHOP <0.9: PRODUCT_MODEL
, ORDER_MODEL
,
ORDER_MODEL_ITEM
, etc. are not required anymore and can safely be removed from our
settings.py
.
Accessing the deferred models¶
Since models in django-SHOP are yet unknown during instantiation, one has to access their materialized instance using the lazy object pattern. For instance in order to access the Cart, use:
from shop.models.cart import CartModel
def my_view(request):
cart = CartModel.objects.get_from_request(request)
cart.items.all() # contains the queryset for all items in the cart
Here CartModel
is a lazy object resolved during runtime and pointing on the materialized, or,
to say it in other words, real Cart model.
Technical Internals¶
Mapping of Foreign Keys¶
One might argue, that this can’t work, since foreign keys must refer to a real model, not to
abstract ones! Therefore one can not add a field ForeignKey
, OneToOneField
or
ManyToManyField
which refers an abstract model in the django-SHOP project. But
relations are fundamental for a properly working software. Imagine a CartItem
without a foreign
relation to Cart
.
Fortunately there is a neat trick to solve this problem. By deferring the mapping onto a real model,
instead of using a real ForeignKey
, one can use a special “lazy” field, declaring a relation
with an abstract model. Now, whenever the models are “materialized”, then these abstract relations
are converted into real foreign keys. The only drawback for this solution is, that one may derive
from an abstract model only once, but for django-SHOP that’s a non-issue and doesn’t differ from
the current situation, where one can subclass BaseCart
only once anyway.
Therefore, when using this deferred model pattern, instead of using models.ForeignKey
,
models.OneToOneField
or models.ManyToManyField
, use the special fields
deferred.ForeignKey
, deferred.OneToOneField
and deferred.ManyToManyField
. When
Django materializes the model, these deferred fields are resolved into real foreign keys.
Accessing the materialized model¶
While programming with abstract model classes, sometimes they must access their model manager
or their concrete model definition. A query such as BaseCartItem.objects.filter(cart=cart)
therefore can not function and will throw an exception. To facilitate this, the deferred model’s
metaclasses adds an additional member _materialized_model
to their base class, while building
the model class. This model class then can be accessed through lazy evaluation, using CartModel
,
CartItemModel
, OrderModel
, OrderItemModel
, etc.
Money Types¶
Until django-SHOP version 0.2, amounts relating to money were kept inside a Decimal
type
and stored in the database model using a DecimalField
. In shop installations with only one
available currency, this wasn’t a major issue, because the currency symbol could be hard-coded
anywhere on the site.
However, for sites offering pricing information in more than one currency, this caused major problems. When we needed to perform calculations with amounts that have an associated currency, it is very common to make mistakes by mixing different currencies. It also is common to perform incorrect conversions that generate wrong results. Python doesn’t allow developers to associate a specific decimal value with a unit.
Starting with version 0.9, django-SHOP now is shipped with a special factory class:
MoneyMaker¶
This class can not be instantiated, but is a factory for building a money type with an associated
currency. Internally it uses the well established Decimal
type to keep track of the amount.
Additionally, it restricts operations on the current Money type. For instance, we can’t sum up
Dollars with Euros. We also can’t multiply two currencies with each other.
Not a Number¶
In special occurrences we’d rather want to specify “no amount” rather than an amount of 0.00 (zero).
This can be useful for free samples, or when an item is currently not available. The Decimal type
denotes a kind of special value a NaN
– for “Not a Number”. Our Money type also knows about
this special value, and when rendered, € –
or $ –``
is printed out.
Declaring a Money object without a value, say m = Money()
creates such a special value. The big
difference as for the Decimal
type is that when adding or subtracting a NaN
to a valid
value, it is considered zero, rather than changing the result of this operation to NaN
as well.
It also allows us to multiply a Money amount with None
. The result of this operation is NaN
.
Create a Money type¶
>>> from shop.money import MoneyMaker
>>> Money = MoneyMaker()
>>> print(Money('1.99'))
€ 1.99
>>> print(Money('1.55') + Money('8'))
€ 9.55
>>> print Money
<class 'shop.money.money_maker.MoneyInEUR'>
>>> Yen = MoneyMaker('JPY')
>>> print(Yen('1234.5678'))
¥ 1235
>>> print(Money('100') + Yen('1000'))
ValueError: Can not add/substract money in different currencies.
How does this work?
By calling MoneyMaker()
a type accepting amounts in the default currency is created.
The default currency can be changed in settings.py
with SHOP_DEFAULT_CURRENCY = 'USD'
,
using one of the official ISO-4217 currency codes.
Alternatively, we can create our own Money type, for instance Yen
.
Formating Money¶
When the amount of a money type is printed or forced to text using str(price)
, it is prefixed
by the currency symbol. This is fine, when working with only a few currencies. However, some symbols
are ambiguous, for instance Canadian, Australian and US Dollars, which all use the “$” symbol.
With the setting SHOP_MONEY_FORMAT
we can style how money is going to be printed out. This
setting defaults to {symbol} {amount}
. The following format strings are allowed:
{symbol}
: The short symbol for a currency, for instance$
,£
,€
,¥
, etc.{code}
: The international currency code, for instance USD, GBP, EUR, JPY, etc.{currency}
: The spoken currency description, for instance “US Dollar”, “Pound Sterling”, etc.{amount}
: The amount, unlocalized.
Thus, if we prefer to print 9.98 US Dollar
, then we should set {amount} {currency}
as the
formatting string.
Localizing Money¶
Since the Money class doesn’t know anything about our current locale setting, amounts always are
printed unlocalized. To localize a Money type, use django.utils.numberformat.format(someamount)
.
This function will return the amount, localized according to the current HTTP request.
Money Database Fields¶
Money can be stored in the database, keeping the currency information together with the field type.
Internally, the database uses the Decimal type, but such a field knows its currency and will return
an amount as MoneyIn...
type. This prevents implicit, but accidental currency conversions.
In our database model, declare a field as:
class Product(models.Model):
...
unit_price = MoneyField(currency='GBP')
This field stores its amounts as British Pounds and returns them typed as MoneyInGBP
.
If the currency
argument is omitted, then the default currency is used.
Money Representation in JSON¶
An additional REST SerializerField has been added to convert amounts into JSON strings. When writing REST serializers, use:
from rest_framework import serializers
from shop.money.rest import MoneyField
class SomeSerializer(serializers.ModelSerializer):
price = MoneyField()
The default REST behavior serializes Decimal types as floats. This is fine if we want to do some computations in the browser using JavaScript. However, then the currency information is lost and must be re-added somehow to the output strings. It also is a bad idea to do commercial calculations using floats, yet JavaScript does not offer any Decimal-like type. It therefore is recommended to always do the finance arithmetic on the server and transfer amount information using JSON strings.
Product Models¶
Products can vary wildly, and modeling them is not always trivial. Some products are salable in pieces, while others are continues. Trying to define a set of product models, capable for describing all such scenarios is impossible –
Describe Products by customizing the Model¶
DjangoSHOP requires to describe products instead of prescribing prefabricated models.
All in all, the merchant always knows best how to describe his products!
E-commerce solutions, claiming to be plug-and-play, usually use one of these (anti-)patterns¶
Either, they offer a field for every possible variation, or they use the Entity Attribute Value (EAV) pattern to add meta-data for each of our models. This at a first glance seems to be easy. But both approaches are unwieldy and have serious drawbacks. They both apply a different “physical schema” – the way data is stored, rather than a “logical schema” – the way users and applications require that data. As soon as we have to combine our e-commerce solution with some Enterprise Resource Planning (ERP) software, additional back-and-forward conversion routines have to be added.
In django-SHOP, the physical representation of a product always maps to its logical¶
Django-SHOP‘s approach to this problem is to have a minimal set of models. These abstract models are stubs providing to subclass the physical models. Hence the logical representation of the product conforms to their physical one. Moreover, it is even possible to represent various types of products by subclassing polymorphically from an abstract base model. Thanks to Django’s Object Relational Mapper, modeling the logical representation for a set of products, together with an administration backend, becomes almost effortless.
Therefore the base class to model a product is a stub which contains only these three fields:
The timestamps for created_at
and updated_at
; these are self-explanatory.
A boolean field active
, used to signalize the products availability.
The attentive reader may wonder, why there not even fields for the most basic requirements of each sellable article, there is no product name, no price field and no product code.
The reason for this is, that django-SHOP does not impose any fields, which might require a different implementation for the merchants use case. However, for a sellable commodity some information is fundamental and required. But its up to him how to implement these fields:
The product’s name must be implemented as a model field or as a property method, but both must be
declared as product_name
. Use a method implementation for composed and translatable names,
otherwise use a database model field with that name.
The product’s price must be implemented as a method declared as get_price(request)
which accepts
the request object. This gives the merchant the ability to vary the price and/or its currency
depending on the geographic location, the customers login status, the browsers user-agent, or
whatever else.
An optional, but highly recommended field is the products item number, declared as
product_code
. It shall return a unique and language independent identifier for each product,
to be identifiable. In most cases the product code is implemented by the product model itself, but
in some circumstances it may be implemented by the product’s variant. The product model
SmartPhone
, as referenced in the demo code, is one such example.
The example section of django-SHOP contains a few models which can be copied and adopted to the specific needs of the merchants products. Let’s have a look at a few use-cases:
Case study: Smart-Phones¶
There are many smart-phone models with different equipment. All the features are the same, except for the built-in storage. How shall we describe such a model?
In that model, the product’s name shall not be translatable, not even on a multi-lingual site, since smart-phones have international names used everywhere. Smart-phones models have dimensions, an operating system, a display type and other features.
But smart-phone have different equipment, namely the built-in storage, and depending on that, they have different prices and a unique product code. Therefore our product models consists of two classes, the generic smart phone model and the concrete flavor of that model.
Therefore we would model our smart-phones using a database model similar to the following one:
from shop.models.product import BaseProductManager, BaseProduct
from shop.money import Money
class SmartPhoneModel(BaseProduct):
product_name = models.CharField(
_("Product Name"),
max_length=255,
)
slug = models.SlugField(_("Slug"))
description = HTMLField(
help_text=_("Detailed description."),
)
manufacturer = models.ForeignKey(
Manufacturer,
verbose_name=_("Manufacturer"),
)
screen_size = models.DecimalField(
_("Screen size"),
max_digits=4,
decimal_places=2,
)
# other fields to map the specification sheet
objects = BaseProductManager()
lookup_fields = ('product_name__icontains',)
def get_price(request):
aggregate = self.variants.aggregate(models.Min('unit_price'))
return Money(aggregate['unit_price__min'])
class SmartPhoneVariant(models.Model):
product_model = models.ForeignKey(
SmartPhoneModel,
related_name='variants',
)
product_code = models.CharField(
_("Product code"),
max_length=255,
unique=True,
)
unit_price = MoneyField(_("Unit price"))
storage = models.PositiveIntegerField(_("Internal Storage"))
Lets go into the details of these classes. The model fields are self-explanatory. Something to note
here is, that each product requires a field product_name
. This alternatively can also be
implemented as a translatable field using django-parler.
Another mandatory attribute for each product is the ProductManager
class. It must inherit
from BaseProductManager
, and adds some methods to generate some special querysets.
Finally, the attribute lookup_fields
contains a list or tuple of lookup fields. These are
required by the administration backend, and used when the site editor has to search for certain
products. Since the framework does not impose which fields are used to distinguish between products,
we must give some hints.
Each product also requires a method implemented as get_price(request)
. This must return the
unit price using one of the available Money Types.
Add multilingual support¶
Adding multilingual support to an existing product is quite easy and straight forward. To achieve this django-SHOP uses the app django-parler which provides Django model translations without nasty hacks. All we have to do, is to replace the ProductManager with one capable of handling translations:
class ProductQuerySet(TranslatableQuerySet, PolymorphicQuerySet):
pass
class ProductManager(BaseProductManager, TranslatableManager):
queryset_class = ProductQuerySet
The next step is to locate the model fields, which shall be available in different languages. In our use-case thats only the product’s description:
class SmartPhoneModel(BaseProduct, TranslatableModel):
# other field remain unchanged
description = TranslatedField()
class ProductTranslation(TranslatedFieldsModel):
master = models.ForeignKey(
SmartPhoneModel,
related_name='translations',
null=True,
)
description = HTMLField(
help_text=_("Some more detailed description."),
)
class Meta:
unique_together = [('language_code', 'master')]
This simple change now allows us to offer the shop’s assortment in different natural languages.
Add Polymorphic Support¶
If besides smart phones we also want to sell cables, pipes or smart cards, we must split our product
models into a common- and a specialized part. That said, we must separate the information every
product requires from the information specific to a certain product type. Say, in addition to smart
phones, we also want to sell smart cards. First we declare a generic Product
model, which is a
common base class of both, SmartPhone
and SmartCard
:
class Product(BaseProduct, TranslatableModel):
product_name = models.CharField(
_("Product Name"),
max_length=255,
)
slug = models.SlugField(
_("Slug"),
unique=True,
)
description = TranslatedField()
objects = ProductManager()
lookup_fields = ['product_name__icontains']
Next we only add the product specific attributes to the class models derived from Product
:
class SmartPhoneModel(Product):
manufacturer = models.ForeignKey(
Manufacturer,
verbose_name=_("Manufacturer"),
)
screen_size = models.DecimalField(
_("Screen size"),
max_digits=4,
decimal_places=2,
)
battery_type = models.PositiveSmallIntegerField(
_("Battery type"),
choices=BATTERY_TYPES,
)
battery_capacity = models.PositiveIntegerField(
help_text=_("Battery capacity in mAh"),
)
ram_storage = models.PositiveIntegerField(
help_text=_("RAM storage in MB"),
)
# and many more attributes as found on the data sheet
class SmartPhone(models.Model):
product_model = models.ForeignKey(SmartPhoneModel)
product_code = models.CharField(
_("Product code"),
max_length=255,
unique=True,
)
unit_price = MoneyField(_("Unit price"))
storage = models.PositiveIntegerField(_("Internal Storage"))
class SmartCard(Product):
product_code = models.CharField(
_("Product code"),
max_length=255,
unique=True,
)
storage = models.PositiveIntegerField(help_text=_("Storage capacity in GB"))
unit_price = MoneyField(_("Unit price"))
CARD_TYPE = [2 * ('{}{}'.format(s, t),)
for t in ('SD', 'SDXC', 'SDHC', 'SDHC II') for s in ('', 'micro ')]
card_type = models.CharField(
_("Card Type"),
choices=CARD_TYPE,
max_length=15,
)
SPEED = [(str(s), "{} MB/s".format(s))
for s in (4, 20, 30, 40, 48, 80, 95, 280)]
speed = models.CharField(
_("Transfer Speed"),
choices=SPEED,
max_length=8,
)
If MyShop would sell the iPhone5 with 16GB and 32GB storage as independent products, then we could
unify the classes SmartPhoneModel
and SmartPhone
and move the attributes product_code
and unit_price
into the class Product
. This would simplify some programming aspects, but
would require the merchant to add a lot of information twice. Therefore we remain with the
model layout presented here.
Caveat using a ManyToManyField
with existing models¶
Sometimes we may need to use a ManyToManyField
for models which are handled by other apps in
our project. This for example could be an attribute files
referring the model
filer.FilerFileField
from the library django-filer. Here Django would try to create a mapping
table, where the foreign key to our product model can not be resolved properly, because while
bootstrapping the application, our Product model is still considered to be deferred.
Therefore, we have to create our own mapping model and refer to it using the through
parameter, as shown in this example:
from six import with_metaclass
from django.db import models
from filer.fields.file import FilerFileField
from shop.models import deferred
from shop.models.product import BaseProductManager, BaseProduct
class ProductFile(with_metaclass(deferred.ForeignKeyBuilder, models.Model)):
file = FilerFileField()
product = deferred.ForeignKey(BaseProduct)
class Product(BaseProduct):
# other fields
files = models.ManyToManyField('filer.File', through=ProductFile)
objects = ProductManager()
Note
Do not use this example for creating a many-to-many field to FilerImageField
.
Instead use shop.models.related.BaseProductImage
which is a base class for this kind
of mapping. Just import and materialize it, in your own project.
Catalog¶
The catalog presumably is that part, where customers of our e-commerce site spend the most time. Often it even makes sense, to start the Catalog List View on the main landing page.
In this documentation we presume that categories of products are built up using specially tagged CMS pages in combination with a djangoCMS apphook. This works perfectly well for most implementation, but some sites may require categories implemented independently of the CMS.
Using an external django-SHOP plugin for managing categories is a very conceivable solution, and we will see separate implementations for this feature request. Using such an external category plugin can make sense, if this e-commerce site requires hundreds of hierarchical levels and/or these categories require a set of attributes which are not available in CMS pages. If you are going to use externally implemented categories, please refer to their documentation, since here we proceed using CMS pages as categories.
A nice aspect of django-SHOP is, that it doesn’t require the programmer to write any special
Django Views in order to render the catalog. Instead all merchant dependent business logic goes
into a serializer, which in this documentation is referred as ProductSerializer
.
Catalog List View¶
In this documentation, the catalog list view is implemented as a django-CMS page. Depending on whether the e-commerce aspect of that site is the most prominent part or just a niche of the CMS, select an appropriate location in the page tree and create a new page. This will become the root of our catalog list.
But first we must Create the CatalogListApp.
Create the CatalogListApp
¶
To retrieve a list of product models, the Catalog List View requires a djangoCMS apphook. This
CatalogListApp
must be added into a file named cms_apps.py
and located in the root folder
of the merchant’s project:
from cms.apphook_pool import apphook_pool
from shop.cms_apphooks import CatalogListCMSApp
class CatalogListApp(CatalogListCMSApp):
def get_urls(self, page=None, language=None, **kwargs):
return ['myshop.urls.products']
apphook_pool.register(CatalogListApp)
as all apphooks, it requires a file defining its urlpatterns:
from django.conf.urls import url
from shop.views.catalog import ProductListView
urlpatterns = [
url(r'^$', ProductListView.as_view()),
# other patterns
]
By default the ProductListView
renders the catalog list of products assigned to the current CMS
page. In this example, only model attributes for fields declared in the default
ProductSummarySerializer
are available in the render context for the used CMS template, as well
as for a representation in JSON suitable for client side rendered list views. This allows us to
reuse this Django View (ProductListView
) whenever the catalog list switches into infinite scroll
mode, where it only requires the product’s summary, composed of plain old JavaScript objects.
Customized Product Serializer¶
In case we need Additional Product Serializer Fields, let’s add them to this class using the serializer fields from the Django RESTFramework library. This can be useful for product serializers which shall provide more information on our catalog list view.
For this customized serializer, we normally only require a few attributes from our model, therefore we can write it as:
from shop.serializers.bases import ProductSerializer
class CustomizedProductSerializer(ProductSerializer):
class Meta:
model = Product
fields = [all-the-fields-required-for-the-list-view]
Additionally, we have to rewrite the URL pattern from above as:
from django.conf.urls import url
from shop.views.catalog import ProductListView
from myshop.serializers import CustomizedProductSerializer
urlpatterns = [
url(ProductListView.as_view(
serializer_class=CustomizedProductSerializer,
)),
# other patterns
]
Here the CustomizedProductSerializer
is used to create a more specialized representation of our
product model.
Add the Catalog to the CMS¶
In the page list editor of django-CMS, create a new page at an appropriate location of the page tree. As the page title and slug we should use something describing our product catalog in a way, both meaningful to the customers as well as to search engines.
Next, we change into Advanced Settings mode.
As a template we use one with a big placeholder, since it must display our list of products.
As Application, select “Catalog List”. This selects the apphook we created in the previous section.
Then we save the page, change into Structure mode and locate the placeholder named Main Content. Add a Container plugin, followed by a Row and then a Column plugin. As the child of this column choose the Catalog List View plugin from section Shop.
Finally we publish the page. If we have assigned products to that CMS page, they should be rendered now.
Catalog Detail View¶
The product’s detail pages are the only ones we typically do not control with django-CMS placeholders. This is because we often have thousands of products and creating a CMS page for each of them, would be kind of overkill. It only makes sense for shops selling up to a dozen of different products.
Therefore, the template used to render the products’s detail view is selected automatically by the
ProductRetrieveView
[1] following these rules:
- look for a template named
<myshop>/catalog/<product-model-name>-detail.html
[2] [3], otherwise - look for a template named
<myshop>/catalog/product-detail.html
[2], otherwise - use the template
shop/catalog/product-detail.html
.
[1] | This is the View class responsible for rendering the product’s detail view. |
[2] | (1, 2) <myshop> is the app label of the project in lowercase. |
[3] | <product-model-name> is the class name of the product model in lowercase. |
Use CMS Placeholders on Detail View¶
If we require CMS functionality for each product’s detail page, its quite simple to achieve. To the
class implementing our product model, add a djangoCMS Placeholder field named placeholder
.
Then add the templatetag {% render_placeholder product.placeholder %}
to the template
implementing the detail view of our product. This placeholder then shall be used to add arbitrary
content to the product’s detail page. This for instance can be an additional text paragraphs,
some images, a carousel or whatever is available from the djangoCMS plugin system.
Route requests on Detail View¶
The ProductsListApp
, which we previously have registered into django-CMS, is able to route
requests on all of its sub-URLs. This is done by expanding the current list of urlpatterns:
from django.conf.urls import url
from shop.views.catalog import ProductRetrieveView
urlpatterns = [
# previous patterns
url(r'^(?P<slug>[\w-]+)$', ProductRetrieveView.as_view()),
# more patterns
]
If we need additional business logic regarding our product, we can create a customized serializer
class, named for instance CustomizedProductDetailSerializer
. This class then may access the
various attributes of our product model, recombine them and/or merge them into a serializable
representation, as described in Customized Product Serializer.
Additionally, we have to rewrite the URL pattern from above as:
from myshop.serializers import CustomizedProductDetailSerializer
urlpatterns = [
# previous patterns
url(r'^(?P<slug>[\w-]+)$', ProductRetrieveView.as_view(
serializer_class=CustomizedProductDetailSerializer,
)),
# more patterns
]
Additional Product Serializer Fields¶
Sometimes such a serializer field shall return a HTML snippet; this for instance is required for
image source (<img src="..." />
) tags, which must thumbnailed by the server when rendered using
the appropriate templatetags from the easythumbnail library. For these use cases add a field
of type media = SerializerMethodField()
with an appropriate method get_media()
to our
serializer class. This method then may forward the given product to a the built-in renderer:
class ProductDetailSerializer(BaseProductDetailSerializer):
# other attributes
def get_media(self, product):
return self.render_html(product, 'media')
This HTML renderer method looks up for a template following these rules:
- look for a template named
<myshop>/product/catalog-<product-model-name>-<second-argument>.html
[4] [5] [6], otherwise - look for a template named
<myshop>/product/catalog-product-<second-argument>.html
[4] [6], otherwise - use the template
shop/product/catalog-product-<second-argument>.html
[6].
[4] | (1, 2) <myshop> is the app label of the project in lowercase. |
[5] | <product-model-name> is the class name of the product model in lowercase. |
[6] | (1, 2, 3) <field-name> is the attribute name of the just declared field in lowercase. |
Emulate Categories¶
Since we want to use CMS pages to emulate categories, the product model must declare a relationship
between the CMS pages and itself. This usually is done by adding a Many-to-Many field named
cms_pages
to our Product model.
As we work with deferred models, we can not use the mapping table, which normally is generated
automatically for Many-to-Many fields by the Django framework. Instead, this mapping table must
be created manually and referenced using the though
parameter, when declaring the field:
from shop.models.product import BaseProductManager, BaseProduct
from shop.models.related import BaseProductPage
class ProductPage(BaseProductPage):
"""Materialize many-to-many relation with CMS pages"""
class Product(BaseProduct):
# other model fields
cms_pages = models.ManyToManyField(
'cms.Page',
through=ProductPage
)
objects = ProductManager()
In this example the class ProductPage
is responsible for storing the mapping information
between our Product objects and the CMS pages.
Admin Integration¶
To simplify the declaration of the admin backend used to manage our Product model, django-SHOP is shipped with a special mixin class, which shall be added to the product’s admin class:
from django.contrib import admin
from shop.admin.product import CMSPageAsCategoryMixin
from myshop.models import Product
@admin.register(Product)
class ProductAdmin(CMSPageAsCategoryMixin, admin.ModelAdmin):
fields = [
'product_name', 'slug', 'product_code',
'unit_price', 'active', 'description'
]
# other admin declarations
This then adds a horizontal filter widget to the product models. Here the merchant must select each CMS page, where the currently edited product shall appear on.
If we are using the method render_html()
to render HTML snippets, these are cached by
django-SHOP, if caching is configured and enabled for that project. Caching these snippets is
highly recommended and gives a noticeable performance boost, specially while rendering catalog list
views.
Since we would have to wait until they expire naturally by reaching their expire time,
django-SHOP offers a mixin class to be added to the Product admin class, to expire all HTML
snippets of a product altogether, whenever a product in saved in the backend. Simply add
shop.admin.product.InvalidateProductCacheMixin
to the ProductAdmin
class described
above.
Note
Due to the way keys are handled in many caching systems, the InvalidateProductCacheMixin
only makes sense if used in combination with the redis_cache backend.
Full Text Search¶
How should a customer find the product he desires in a more or less unstructured collection of countless products? Hierarchical navigation often doesn’t work and takes too much time. Thanks to the way we use the Internet today, most site visitors expect one central search field in, or nearby the main navigation bar of a site.
Search Engine API¶
In Django the most popular API for full-text search is Haystack. While other indexing backends, such as Solr and Whoosh might work as well, the best results have been achieved with Elasticsearch. Therefore this documentation focuses exclusively on Elasticsearch. And since in django-SHOP every programming interface uses REST, search is no exception here. Fortunately there is a project named drf-haystack, which “restifies” our search results, if we use a special serializer class.
In this document we assume that the merchant only wants to index his products, but not any arbitrary content, such as for example the terms and condition, as found outside django-SHOP, but inside django-CMS. The latter would however be perfectly feasible.
Configuration¶
Install the Elasticsearch binary. Currently Haystack only supports versions smaller than 2. Then start the service in daemon mode:
./path/to/elasticsearch-version/bin/elasticsearch -d
Check if the server answers on HTTP requests. Pointing a browser onto port http://localhost:9200/ should return something similar to this:
$ curl http://localhost:9200/
{
"status" : 200,
"name" : "Ape-X",
"cluster_name" : "elasticsearch",
"version" : {
...
},
}
In settings.py
, check that 'haystack'
has been added to INSTALLED_APPS
and connects
the application server with the Elasticsearch database:
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
'URL': 'http://localhost:9200/',
'INDEX_NAME': 'myshop-default',
},
}
In case we need indices for different natural languages on our site, we shall add the non-default
languages to this Python dictionary using a different INDEX_NAME
for each of them.
Finally configure the site, so that search queries are routed to the correct index using the currently active natural language:
HAYSTACK_ROUTERS = ('shop.search.routers.LanguageRouter',)
Indexing the Products¶
Before we start to search for something, we first must populate its indices. In Haystack one can create more than one kind of index for each item being added to the search database.
Each product type requires its individual indexing class. Note that Haystack does some
autodiscovery, therefore this class must be added to a file named search_indexex.py
. For our
product model SmartCard
, this indexing class then may look like:
from shop.search.indexes import ProductIndex
from haystack import indexes
class SmartCardIndex(ProductIndex, indexes.Indexable):
catalog_media = indexes.CharField(stored=True,
indexed=False, null=True)
search_media = indexes.CharField(stored=True,
indexed=False, null=True)
def get_model(self):
return SmartCard
# more methods ...
While building the index, Haystack performs some preparatory steps:
Populate the reverse index database¶
The base class for our search index declares two fields for holding the reverse indexes and a few additional fields to store information about the indexed product entity:
class ProductIndex(indexes.SearchIndex):
text = indexes.CharField(document=True,
indexed=True, use_template=True)
autocomplete = indexes.EdgeNgramField(indexed=True,
use_template=True)
product_name = indexes.CharField(stored=True,
indexed=False, model_attr='product_name')
product_url = indexes.CharField(stored=True,
indexed=False, model_attr='get_absolute_url')
The first two index fields require a template which renders plain text, which is used to build a
reverse index in the search database. The indexes.CharField
is used for a classic reverse text
index, whereas the indexes.EdgeNgramField
is used for autocompletion.
Each of these index fields require their own template. They must be named according to the following rules:
search/indexes/myshop/<product-type>_text.txt
and
search/indexes/myshop/<product-type>_autocomplete.txt
and be located inside the project’s template folder. The <product-type>
is the classname in
lowercase of the given product model. Create two individual templates for each product type, one
for text search and one for autocompletion.
An example:
{{ object.product_name }}
{{ object.product_code }}
{{ object.manufacturer }}
{{ object.description|striptags }}
{% for page in object.cms_pages.all %}
{{ page.get_title }}{% endfor %}
The last two fields are used to store information about the product’s content, side by side with the indexed entities. That’s a huge performance booster, since this information otherwise would have to be fetched from the relational database, item by item, and then being rendered while preparing the search query result.
We can also add fields to our index class, which stores pre-rendered HTML. In the above example,
this is done by the fields catalog_media
and search_media
. Since we do not provide
a model attribute, we must provide two methods, which creates this content:
class SmartCardIndex(ProductIndex, indexes.Indexable):
# other fields and methods ...
def prepare_catalog_media(self, product):
return self.render_html('catalog', product, 'media')
def prepare_search_media(self, product):
return self.render_html('search', product, 'media')
These methods themselves invoke render_html
which takes the product and renders it using
a templates named catalog-product-media.html
or search-product-media.html
respectively.
These templates are looked for in the folder myshop/products
or, if not found there in the
folder shop/products
. The HTML snippets for catalog-media are used for autocompletion search,
whereas search-media is used for normal a normal full-text search invocation.
Building the Index¶
To build the index in Elasticsearch, invoke:
./manage.py rebuild_index --noinput
Depending on the number of products in the database, this may take some time.
Search Serializers¶
Haystack for Django REST Framework is a small library aiming to simplify using Haystack with Django REST Framework. It takes the search results returned by Haystack, treating them the similar to Django database models when serializing their fields. The serializer used to render the content for this demo site, may look like:
from rest_framework import serializers
from shop.search.serializers import ProductSearchSerializer as ProductSearchSerializerBase
from .search_indexes import SmartCardIndex, SmartPhoneIndex
class ProductSearchSerializer(ProductSearchSerializerBase):
media = serializers.SerializerMethodField()
class Meta(ProductSearchSerializerBase.Meta):
fields = ProductSearchSerializerBase.Meta.fields + ('media',)
index_classes = (SmartCardIndex, SmartPhoneIndex)
def get_media(self, search_result):
return search_result.search_media
This serializer is part of the project, since we must adopt it to whatever content we want to display on our site, whenever a visitor enters some text into the search field.
Search View¶
In the Search View we link the serializer together with a djangoCMS apphook. This
CatalogSearchApp
can be added to the same file, we already used to declare the
CatalogListApp
used to render the catalog view:
from cms.apphook_pool import apphook_pool
from shop.cms_apphooks import CatalogSearchCMSApp
class CatalogSearchApp(CatalogSearchCMSApp):
def get_urls(self, page=None, language=None, **kwargs):
return ['myshop.urls.search']
apphook_pool.register(CatalogSearchApp)
as all apphooks, it requires a file defining its urlpatterns:
from django.conf.urls import url
from shop.search.views import SearchView
from myshop.serializers import ProductSearchSerializer
urlpatterns = [
url(r'^', SearchView.as_view(
serializer_class=ProductSearchSerializer,
)),
]
Display Search Results¶
As with all other pages in django-SHOP, the page displaying our search results is a normal CMS page too. It is suggested to create this page on the root level of the page tree.
As the page title use “Search” or whatever is appropriate as expression. Then we change into the Advanced Setting od the page.
As a template use one with a big placeholder, since it must display our search results. Our default template usually is a good fit.
As the page Id field, enter shop-search-product
. Some default HTML snippets, prepared for
inclusion in other templates, use this hard coded string.
Set the input field Soft root to checked. This hides our search results page from the menu list.
As Application, select “Search”. This selects the apphook we created in the previous section.
Then save the page, change into Structure mode and locate the placeholder named Main Content. Add a Bootstrap Container plugin, followed by a Row and then a Column plugin. As the child of this column, choose the Search Results plugin from section Shop.
Finally publish the page and enter some text into the search field. It should render a list of found products.
Autocompletion in Catalog List View¶
As we have seen in the previous example, the Product Search View is suitable to search for any item in the product database. However, the site visitor sometimes might just refine the list of items shown in the catalog’s list view. Here, loading a new page which uses a layout able to render every kind of product usually differs from the catalog’s list layout, and hence may by inappropriate.
Instead, when someone enters some text into the search field, django-SHOP starts to narrow down the list of items in the catalog’s list view by typing query terms into the search field. This is specially useful in situations where hundreds of products are displayed together on the same page and the customer needs to pick out the correct one by entering some search terms.
To extend the existing Catalog List View for autocompletion, locate the file containing the
urlpatterns, which are used by the apphook ProductsListApp
. In doubt, consult the file
myshop/cms_apps.py
. This apphook names a file with urlpatterns. Locate that file and add the
following entry:
In order to use the Product Search View, our Product Model must inherit from
shop.models.product.CMSPageReferenceMixin
. This is because we must add a reference to the
CMS pages our products are assigned to, into the search index database. Such a product may for
instance be declared as:
from shop.models.product import BaseProduct, BaseProductManager, CMSPageReferenceMixin
class MyProduct(CMSPageReferenceMixin, BaseProduct):
...
objects = BaseProductManager()
...
We normally want to use the same URL to render the catalog’s list view, as well as the autocomplete view, and hence must route onto the same view class. However the search- and the catalog’s list view classes have different bases and a completely different implementation.
The normal List View uses a Django queryset to iterate over the products, while the autocomplete
View uses a Haystack Search queryset. Therefore we wrap both View classes into
shop.search.views.CMSPageCatalogWrapper
and use it in our URL routing such as:
from django.conf.urls import url
from shop.search.views import CMSPageCatalogWrapper
from myshop.serializers import CatalogSearchSerializer
urlpatterns = [
url(r'^$', CMSPageCatalogWrapper.as_view(
search_serializer_class=CatalogSearchSerializer,
)),
# other patterns
]
The view class CMSPageCatalogWrapper
is a functional wrapper around the catalog’s products list
view and the search view. Depending on whether the request contains a search query starting with
q=<search-term>
, either the search view or the normal products list view is invoked.
The CatalogSearchSerializer
used here is very similar to the ProductSearchSerializer
, we
have seen in the previous section. The only difference is, that instead of the search_media
field is uses the catalog_media
field, which renders the result items media in a layout
appropriate for the catalog’s list view. Therefore this kind of search, normally is used in
combination with auto-completion, because here we reuse the same layout for the product’s list view.
The Client Side¶
To facilitate the placement of the search input field, django-SHOP ships with a reusable
AngularJS directive shopProductSearch
, which is declared inside the module
shop/js/search-form.js
.
A HTML snipped with a submission form using this directive can be found in the shop’s templates
folder at shop/navbar/search-form.html
. If you override it, make sure that the form element
uses the directive shop-product-search
as attribute:
<form shop-product-search method="get" action="/url-of-page-rendering-the-search-results">
<input name="q" ng-model="searchQuery" ng-change="autocomplete()" type="text" />
</form>
If you don’t use the prepared HTML snippet, assure that the module is initialized while bootstrapping our Angular application:
angular.module('myShop', [..., 'django.shop.search', ...]);
Filter Products by its Attributes¶
Besides Full Text Search, adding a filtering functionality to an e-commerce site is another very important feature. Customers must be able to narrow down a huge list of available products to a small set of desired products using a combination of prepared filter attributes.
In django-SHOP, we model each product according to its own properties, the color for instance. The customer then might filter the listed products, selecting one or more of the given properties, the color “blue” for instance.
Therefore, when creating a database schema, we add that property to our product model. This can either be a hard coded list of enumerated choices, or if we need a more flexible approach, a foreign key onto another model referring to that specific property. If our product model allows more than one attribute of the same property, then we would use a many-to-many-key in our database.
The contents of this additional property model (or hard coded property list), then is used to create a set of available filtering options, from which the customer can select one (if allowed, also more) options to narrow down the list of products with that specific attributes.
Fortunately, the REST framework in combination with Django Filter, makes it a rather simple task to implement this kind of filtering functionality on top of the existing product models.
Adding a Filter to the List View¶
In django-SHOP showing a list of products, normally is controlled by the classes
shop.views.catalog.ProductListView
or shop.views.catalog.CMSPageProductListView
.
By default these View classes are configured to use the default filter backends as provided by the
REST framework. These filter backends can be configured globally through the settings variable
DEFAULT_FILTER_BACKENDS
.
Additionally we can subclass the filter backends for each View class in our urls.py
. Say, we
need a special catalog filter, which groups our products by a certain product attribute. Then we
can create a customized filter backend
from rest_framework.filters import BaseFilterBackend
class CatalogFilterBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
queryset = queryset.order_by('attribute__sortmetric')
return queryset
In urls.py
, where we route requests to the class shop.views.catalog.ProductListView
,
we then replace the default filter backends by our own implementation:
from django.conf.urls import url
from rest_framework.settings import api_settings
from shop.views.catalog import ProductListView
urlpatterns = [
url(r'^$', ProductListView.as_view(
filter_backends=[CatalogFilterBackend],
),
]
The above example is very simple but gives a rough impression on its possibilities.
Working with Django-Filter¶
Django Filter is a generic, reusable application to alleviate writing some of the more mundane bits of view code. Specifically, it allows users to filter down a queryset based on a model’s fields, displaying the form to let them do this.
REST framework also includes support for generic filtering backends that allow you to easily construct complex searches and filters.
By creating a class which inherits from django_filters.FilterSet
, we can build filters
against each attribute of our product. This filter then uses the passed in query parameters to
restrict the set of products available from our current catalog view. Presume that our product model
uses a foreign key onto a model holding all manufactures. We then can create a simple filter class
to restrict our list view onto a certain manufacturer:
from django.forms import forms, widgets
import django_filters
from djng.forms import NgModelFormMixin
from myshop.models.product import MyProduct, Manufacturer
class FilterForm(NgModelFormMixin, forms.Form):
scope_prefix = 'filters'
class ProductFilter(django_filters.FilterSet):
manufacturer = django_filters.ModelChoiceFilter(
queryset=Manufacturer.objects.all(),
widget=Select(attrs={'ng-change': 'filterChanged()'}),
empty_label="Any Manufacturer")
class Meta:
model = MyProduct
form = FilterForm
fields = ['manufacturer']
@classmethod
def get_render_context(cls, request, queryset):
"""
Prepare the context for rendering the filter.
"""
filter_set = cls()
# we only want to show manufacturers for products available in the current list view
filter_field = filter_set.filters['manufacturer'].field
filter_field.queryset =filter_field.queryset.filter(
id__in=queryset.values_list('manufacturer_id'))
return dict(filter_set=filter_set)
To this filter class we can combine as many fields as we need, but in this example, we just use the foreign key to the manufacturer model. For all available filter field types, please check the appropriate documentation in Django Filter.
We then can add this filter class to our product list view. In django-SHOP this normally is done through the url patterns:
urlpatterns = [
url(r'^$', ProductListView.as_view(
filter_class=ProductFilter,
)),
# other patterns
]
By appending ?manufacturer=7
to the URL, the above filter class will restrict the products
in our list view to those manufactured by the database entry with a primary key of 7.
Populate the Render Context¶
Filtering functionality without an appropriate user interface doesn’t make much sense. Therefore, when rendering the product’s list view, we might want to add some input fields or special links, so that the customer can narrow down the result set. To do this, the rendering template requires additional context data.
Since django-SHOP honours the principle of cohesion, each filter set is responsible for providing
the context required to render its specific filtering parameters. This extra context must be
provided by a class-method named get_render_context(request, queryset)
, which must return
a dictionary containing an instance of that filter set.
While rendering HTML pages, this extra context then can be used to render various tag filtering
elements, such as a <select>
-box. Since our ProductFilter
can be rendered as form fields,
we just have to use this Django template:
..code-block:: django
{{ filter.filter_set.form }}
The Client Side¶
If your site uses the provided AngularJS directive <shop-list-products>
, we typically want to
use that as well, when the customer applies a product filter. Therefore this directive listens on
events named shopCatalogFilter
and queries the backend with the given properties. This allows
us to add a set of filter options to the product’s list view, without having to care about how to
fetch that filtered list from the server.
Since we don’t event want to care about controlling change events on the filtering <select>
box,
django-SHOP is shipped with a reusable directive named shopProductFilter
.
Sample HTML snippet:
<div shop-product-filter="manufacturer">
{{ filter.filter_set.form }}
</div>
or if your filter set forms uses more than one attribute:
<div shop-product-filter="['manufacturer', 'brand']">
{{ filter.filter_set.form }}
</div>
The Angular directive shopProductFilter
is declared inside the shop’s shop/js/filters.js
module, so make sure to include that file. Additionally, that module must be initialized while
bootstrapping our Angular application:
angular.module('myShop', [..., 'django.shop.filter', ...]);
Each time the customer selects another manufacturer, the function filterChanged
emits
an event intercepted by the AngularJS directive shopListProducts
, which consequently
fetches a list of products using the filtering class as shown above.
Apart from forwarding changes detected in our <select>
box, this directive also modifies the
URL and appends the selected properties. This is required, whenever the user navigates away from
the product’s list view and returns back, so that the same filters are applied. Additionally the
directive clears the search query field, because full text search in combination with property
filtering is confusing and doesn’t make sense.
Cascade Plugins¶
DjangoSHOP extends the eco-system of djangoCMS plugins, djangocms-cascade, by additional shop-specific plugins. This allows us to create a whole shopping site, which consists of many different elements, without having to craft templates by hand – with one exception: The product detail views.
Therefore all we have to focus on, is a default page template with one big placeholder. This placeholder then is subdivided into containers, rows, columns and other elements of the Cascade plugin collection.
This however requires a completely different approach, from the designer point of view. The way web design has been done a few years ago, starting with the screenshot of a finished page, must be rethought. This has been discussed in length by many web-designers, especially by Brad Frost in his excellent book on Atomic Web Design. He propagates to reverse the design process and start with the smallest entity, which he calls Atoms. They form to bigger components, named Molecules, which themselves aggregate to Organisms.
Some designers nowadays build those components directly in HTML and CSS or SASS, instead of drawing their screens using programs such as InDesign or PhotoShop (which by the way never was intended for this kind of work). It also exempts having the programmer to convert those screens into HTML and CSS – a time consuming and never satisfying job.
According to Frost, the next bigger component after the Organism is the template. This is where djangocms-cascade jumps in. Each of the Cascade plugins is shipped with its own default template, which can easily be overwritten by the designers own implementation.
Overriding Templates¶
For all plugins described here, we can override the provided templates with our own implementation.
If the shop framework provides a template, named /shop/folder/my-organism.html
, then we may
override it using /merchantimplementaion/folder/my-organism.html
.
This template then usually extends the existing framework template with
{% extends "/shop/folder/my-organism.html" %}
{% block shop-some-identifier %}
<div>...</div>
{% endblock %}
This is in contrast to Django’s own implementation for searching the template, but allows to extend exiting templates more easily.
Breadcrumb¶
The BreadcrumbPlugin has four different rendering options: Default, Soft-Root, With Catalog Count and Empty. It can be added exclusively to the placeholder named Breadcrumb, unless otherwise configured.
The Default breadcrumb behaves as expected. Soft-Root appends the page title to the existing breadcrumb, it shall be used for pages marked as soft root. A breadcrumb of type With Catalog Count adds a badge containing the number of items. Use an Empty to hide the breadcrumb otherwise displayed by the placeholder as default.
Cart¶
The CartPlugin has four different rendering options: Editable, Static, Summary and Watch-List. Refer to the Cart using a Cascade Plugin for details.
Checkout Forms¶
All Forms added to the checkout page are managed by members of the Cascade plugin system. All these
plugin inherit from a common base class, shop.cascade.plugin_base.DialogFormPluginBase
.
They all have in common to render and validate one specific Form, which itself inherits from
shop.forms.DialogForm
or shop.forms.DialogModelForm
.
A nice aspect of this approach is, that ...
- if we add, change or delete attributes in a form, fields are added, changed or deleted from the rendered HTML as well.
- we get client side form validation for free, without having to write any Javascript nor HTML.
- if we add, change or delete attributes in a form, this modification propagates down to both form validation controllers: That one in Javascript used on the client as well as the final one, validating the form on the server.
- if our forms are made out of models, all of the above works as well.
- we can arrange each of those form components using the Structure editor from djangoCMS toolbar. This is much faster, than by crafting templates manually.
As we can see from this approach, django-SHOP places great value on the principles of a Single Source of Truth, when working with customized database models and forms.
Many of these Forms can be rendered using two different approaches:
Form dialog¶
Here we render all model fields as input fields and group them into an editable form. This is the normal use case.
Static summary¶
Here we render all model fields as static strings without wrapping it into a form. This shall be used to summarize all inputs, preferably on the last process step.
These are the currently available plugins provided by django-SHOP to build the checkout page:
Customer Form Plugin¶
The Customer Form is used to query information about some personal information, such as
the salutation, the first- and last names, its email address etc. In simple terms, this form
combines the fields from the model classes shop.models.customer.Customer
and
email_auth.models.User
or auth.models.User
respectively. This means that fields,
we add to our Customer
model, are reflected automatically into this form.
Guest Form Plugin¶
The Guest Form is a reduced version of the Customer Form. It only asks for the email address, but nothing else. We use it for customers which do not want to create an account.
Shipping- and Billing Address Forms¶
There are two form plugins, where customers can add their shipping and/or billing address. The
billing address offers a checkbox allowing to reuse the shipping address. By overriding the form
templates, this behavior can be switched.
Both plugins provide a form made up from the model class implementing
shop.models.address.AddressModel
.
Select the Payment Provider¶
For each payment provider registered within django-SHOP, this plugin creates a list of radio buttons, where customers can choose their desired payment provider. By overriding the rendering templates, additional forms, for instance to add credit card data, can be added.
Select a Shipping Method¶
For each shipping provider registered within django-SHOP, this plugin creates a list of radio buttons, where customers can choose their desired shipping method.
Extra Annotations Plugin¶
This plugin provides a form, where customers can enter an extra annotation, while they proceed through the checkout process.
Accept Condition Plugin¶
Normally customers must click onto a checkbox to accept various legal requirements, such as the terms and conditions of this site. This plugin offers a text editor, where the merchant can enter a paragraph, possibly with a link onto another CMS page explaining them in more details.
Required Form Fields Plugin¶
Most checkout forms have one or more required fields. To labels of required input fields, an asterisk is appended. This plugin can be used to add a short text message stating “* These fields are required”. It normally should be placed between the last checkout form and the proceed button.
Proceed Button¶
This plugin adds a styleable proceed button to any placeholder. This kind of button differs from a clickable link button in that sense, that it first sends all gathered form data to the server and awaits a response. Only if all forms are successfully validated, this button proceeds to the given link.
This proceed button can also handle two non-link targets: “Reload Page” and “Purchase Now”.
The first target is useful to reload the page in a changed context, for instance if a site visitor logged in and now shall get a personalized page.
The second target is special to django-SHOP and exclusively used, when the customer performs The Purchasing Operation.
Authentication¶
Before proceeding with various input forms, we must know the authentication status of our site visitors. These different states are explained here in detail: Anonymous Users and Visiting Customers.
Therefore we need pluggable forms, where visitors can sign in and out, change and rest passwords and so on. All this authentication forms are handled by one single plugin
Authentication Plugin¶
This plugin handles a bunch of authentication related forms. Lets list them:
Login Form¶
This is a simple login form accepting a username and password.
This form normally is used in combination with Link type: CMS Page.
Logout Form¶
This logout form just adds a button to sign out from the site.
This form normally is used in combination with Link type: CMS Page.
Password Reset Form¶
This form offers a field, so that registered users, which forgot their password, can enter their email address to start a password reset procedure.
Login & Reset Form¶
This extends the Shared Login/Logout Form by combining it with the Password Reset Form form.
If someone clicks on the link Password Forgotten? the form extends to
This form normally is used in combination with Link type: Reload Page.
Change Password Form¶
This form offers two field to change the password. It only appears for logged in users.
Register User Form¶
Using this form, anonymous visitors can register themselves. After having entered their email address and their desired passwords, they become registered users.
This form normally is used in combination with Link type: Reload Page.
Continue as Guest Form¶
This form just adds a button, so that visitors can declare themselves as guest users who do not want to register an account, nor expose their identity.
This form normally is used in combination with Link type: Reload Page.
Process Bar¶
The ProcessBarPlugin can be used to group many forms plugins onto the same page, by dividing them up into different block. Only one block is visible at a time. At to top of that page, a progress bar appears which shows the active step.
This plugin checks the validity of all of its forms and allows to proceed to the next step only, if all of them are valid.
Each step in that process bar must contain a Next Step Button, so that the customer can move to the next step, provided all forms are valid.
The last step shall contain a Proceed Button which shall be configured to take appropriate action, for instance to start the purchasing operation using the Link type “Purchase Now”.
Note
This plugin requires the AngularJS directive <bsp-process-bar>
as found in the
npm package angular-bootstrap-plus.
Catalog¶
The catalog list view is handled by the ShopCatalogPlugin.
This plugin requires a CMS page, which uses the apphook ProductsListApp. First assure that we Create the CatalogListApp. This CMSapp must be implemented by the merchant; it thus is part of the project, rather than the django-SHOP framework.
Viewing Orders¶
The Order Views plugin is used to render the list- and detail views of orders, specific to the currently logged in customer. Without a number in the URL, a list of all orders belonging to the current customer is shown. By adding the primary key of a specific order to the URL, all ordered items from that specific order are shown. We name this the order detail view, although it is a list of items.
This plugin requires a CMS page, which as uses the CMSApp OrderApp. This CMS application is part of the shop framework and always available in the Advanced Settings of each CMS page.
Caveat when editing the Order Detail Page¶
The Order List- and Detail Pages share one common entity in our CMS page tree. The Order Detail view just rendered in a different way. Editing this pseudo page therefore is not possible because it is not part of the CMS.
Search Results¶
Rendering search results is handled by the Search Results plugin.
On a site offering full-text search, add a page to display search results. First assure that we have a Search View assigned to that page as apphook. This CMSapp must be implemented by the merchant; it thus is part of the project, rather than the django-SHOP framework.
Cart and Checkout¶
In django-SHOP the cart’s content is always stored inside the database. In previous versions of the software, the cart’s content was kept inside the session for anonymous users and stored in the database for logged in users. Now the cart is always stored in the database. This approach simplifies the code and saves some random access memory, but adds another minor problem:
From a technical point of view, the checkout page is the same as the cart. They can both be on separate pages, or be merged on the same page. Since what we would normally name the “Checkout Page”, is only a collection of Cascade Plugins, we won’t go into further detail here.
Expired Carts¶
Sessions expire, but then the cart’s content of anonymous customers still remains in the database. We therefore must assure that these carts will expire too, since they are of no use for anybody, except, maybe for some data-mining experts.
By invoking
./manage.py shopcustomers
Customers in this shop: total=3408, anonymous=140, expired=88,
active=1108, guests=2159, registered=1109, staff=5.
we gather some statistics about customers having visited of our django-SHOP site. In this example we see that 1109 customers bought as registered users, while 2159 bought as guests. There are 88 customers in the database, but they don’t have any associated session anymore, hence they can be considered as expired. Invoking
./manage.py shopcustomers --delete-expired
deletes those expired customers, and with them their expired carts. This task shall be performed by a cronjob on a daily or weekly basis.
Cart Models¶
The cart consists of two models classes Cart
and CartItem
, both inheriting from BaseCart
and BaseCartItem
respectively. As with most models in django-SHOP, these are using the
Deferred Model Pattern, so that inheriting from a base class automatically sets the
foreign keys to the appropriate model. This gives the programmer the flexibility to add as many
fields to the cart, as the merchant requires for his special implementation.
In most use-cases, the default cart implementation will do the job. These default classes can be
found at shop.models.defaults.cart.Cart
and shop.models.defaults.cart_item.CartItem
.
To materialize the default implementation, it is enough to import
these two files into the
merchants shop project. Otherwise we create our own cart implementation inheriting from BaseCart
and BaseCartItem
. Since the item quantity can not always be represented by natural numbers, this
field must be added to the CartItem
implementation rather than its base class. Its field type
must allow arithmetic operations, so only IntegerField
, FloatField
or DecimalField
are allowed as quantity.
Note
Assure that the model CartItem
is imported (and materialized) before model
Product
and classes derived from it.
The Cart
model uses its own manager. Since there is only one cart per customer, accessing the
cart must be performed using the request
object. We can always access the cart for the current
customer by invoking:
from shop.models.cart import CartManager
cart = CartModel.objects.get_or_create_from_request(request)
Adding a product to the cart, must be performed by invoking:
from shop.models.cart import CartItemManager
cart_item = CartItemManager.get_or_create(
cart=cart, product=product, quantity=quantity, **extras)
This returns a new cart item object, if the given product could not be found in the current cart.
Otherwise it returns the existing cart item, increasing the quantity by the given value. For
products with variations it’s not always trivial to determine if they shall be considered as
existing cart items, or as new ones. Since django-SHOP can’t tell that difference for any kind
of product, it delegates this question. Therefore the class implementing the shop’s products shall
override their method is_in_cart
. This method is used to tell the CartItemManager
whether a
product has already been added to the cart or is new.
Whenever the method cart.update(request)
is invoked, the cart modifiers run against all items
in the cart. This updates the line totals, the subtotal, extra costs and the final sum.
Watch List¶
Instead of implementing a separate watch-list (some would say wish-list), django-SHOP uses a simple trick. Whenever the quantity of a cart item is zero, this item is considered to be in the watch list. Otherwise it is considered to be in the cart. The train of though is as follows: A quantity of zero, never makes sense for items in the cart. On the other side, any quantity makes sense for items in the watch-list. Therefore reducing the quantity of a cart item to zero is the same as keeping an eye on it, without actually wanting it to purchase.
Cart Views¶
Displaying the cart in django-SHOP is as simple, as adding any other page to the CMS. Change into the Django admin backend and enter into the CMS page tree. At an appropriate location in that tree add a new page. As page title use “Cart”, “Basket”, “Warenkorb”, “Cesta”, or whatever is appropriate in the natural language used for that site. Multilingual CMS installations offer a page title for each language.
In the CMS page editor click onto the link named Advanced Settings at the bottom of the popup window. As template, choose the default one, provided it contains at least one big placeholder.
Enter “shop-cart” into the Id-field just below. This identifier is required by some templates which link directly onto the cart view page. If this field is not set, some links onto the cart page might not work properly.
It is suggested to check the checkbox named Soft root. This prevents that a menu item named “Cart” will appear side by side with other pages from the CMS. Instead, we prefer to render a special cart symbol located on the right of the navigation bar.
Cart using a Cascade Plugin¶
Click onto View on site and change into front-end editing mode to use the grid-system of djangocms-cascade. Locate the main placeholder and add a Row followed by at least one Column plugin; both can be found in section Bootstrap. Below that column plugin, add a child named Cart from section Shop. This Cart Plugin can be rendered in four different ways:
Editable Cart¶
An “Editable Cart” is rendered using the Angular JS template engine. This means that a customer may change the number of items, delete them or move them to the watch-list. Each update is reflected immediately into the cart’s subtotal, extra fields and final totals.
Using the above structure, the rendered cart will look similar to this.
Depending on the chosen template, this layout may vary.
Static Cart¶
An alternative to the editable cart is the static cart. Here the cart items are rendered by the Django template engine. Since here everything is static, the quantity can’t be changed anymore and the customer would have to proceed to the checkout without being able to change his mind. This probably only makes sense when purchasing a single product.
Cart Summary¶
This only displays the cart’s subtotal, the extra cart fields, such as V.A.T., shipping costs and the final total.
Watch List¶
A special view of the cart is the watch list. It can be used by customers to remember items they want to compare or buy sometimes later. The watch-list by default is editable, but does not allow to change the quantity. This is because the watch-list shares the same object model as the cart items. If the quantity of an item 0, then that cart item is considered to be watched. If instead the quantity is 1 ore more, the item is considered to be in the cart. It therefore is very easy to move items from the cart to the watch-list and vice versa. This concept also disallows to have an item in both the cart and the watch-list. This during online shopping, often can be a major point of confusion.
Render templates¶
The path of the templates used to render the cart views is constructed using the following rules:
- Look for a folder named according to the project’s name, ie.
settings.SHOP_APP_LABEL
in lower case. If no such folder can be found, then use the folder namedshop
. - Search for a subfolder named
cart
. - Search for a template named
editable.html
,static.html
,watch.html
orsummary.html
.
These templates are written to be easily extensible by the customized templates. To override the
“editable cart” add a template with the path, say myshop/cart/editable.html
to the projects
template folder. This template then shall begin with {% extend "shop/cart/editable.html" %}
and only override the {% block %}...{% endblock %}
interested in.
Many of these template blocks are themselves embedded inside HTML elements such as
<script id="shop/....html" type="text/ng-template">
. The reason for this is that the editable
cart is rendered in the browser by AngularJS using so called directives. Hence it becomes very
straight-forward to override Angular’s script templates using Django’s internal template engine.
If for some special reasons we need different cart templates, then we must add this line to the
projects settings.py
:
CMSPLUGIN_CASCADE_PLUGINS_WITH_EXTRA_RENDER_TEMPLATES = {
'ShopCartPlugin': (
(None, _("default")), # the default behavior
('myproject/cart/other-editable.html', _("extra editable")),
)
}
This will add an extra select button to the cart editor. The site administrator then can choose between the default template and an extra editable cart template.
On the cart’s view, the merchant may decide whether to implement the checkout forms together with the cart, or to create a special checkout page onto which the customer can proceed. From a technical point of view, it doesn’t make any difference, if the cart and the checkout are combined on the same CMS page, or if they are split across two or more pages. In the latter case simply add a button at the end of each page, so that the customer can easily proceed to the next one.
On the checkout page, the customer has to fill out a few forms. These can be a contact form, shipping and billing addresses, payment and shipping methods, and many more. Which ones depend on the configuration, the legal regulations and the requirements of the shop’s implementation. In Cascade Plugins all shop specific CMS plugins are listed. They can be combined into whatever makes sense for a successful checkout.
Add a Cart via manually written Cart Template¶
Instead of using the CMS plugin system, the template for the cart can also be implemented manually.
Based on an existing page template, locate the element, where the cart shall be inserted. Then
use one of the existing templates in the folder django-shop/shop/templates/shop/cart/
as a
starting point, and insert it at an appropriate location in the page template. Next, in the
project’s settings.py
, add this specialized template to the list CMS_TEMPLATES
and select
it for that page.
From a technical point of view, it does not make any difference whether we use the cart plugin or a handcrafted template. If the HTML code making up the cart has to be adopted to the merchants needs, we normally are better off and much more flexible, if we override the template code as described in section Render templates. Therefore, it is strongly discouraged to craft cart and checkout templates by hand.
Cart Modifiers¶
Cart Modifiers are simple plugins that allow the merchant to define rules in a programmatic way, how the totals of a cart are computed and how they are labeled. A typical job is to compute tax rates, adding discounts, shipping and payment costs, etc.
Instead of implementing each possible combination for all of these use cases, the django-SHOP framework offers an API, where third party applications can hooks into every computational step. One thing to note here is that Cart Modifiers are not only invoked, when the cart is complete and the customer wants to proceed to the checkout, but also for each item before being added to the cart.
This allows the programmer to vary the price of certain items, depending on the current state of the cart. It can for instance be used, to set one price for the first item, and other prices for every further items added to the cart.
Cart Modifiers are split up into three different categories: Generic, Payment and Shipping. In the
shops settings.py
they must be configured as a list or tuple such as:
SHOP_CART_MODIFIERS = (
'shop.modifiers.defaults.DefaultCartModifier',
'shop.modifiers.taxes.CartExcludedTaxModifier',
'myshop.modifiers.PostalShippingModifier',
'shop.modifiers.defaults.PayInAdvanceModifier',
'shop_stripe.modifiers.StripePaymentModifier',
)
Generic modifiers are applied always. The Shipping and Payment modifiers are applied only for the selected shipping and/or payment method. If the customer has not yet decided, how to ship or how to pay, then the corresponding modifiers are not applied.
When updating the cart, modifiers are applied in the order of the above list. Therefore it makes a difference, if taxes are applied before or after having applied the shipping costs.
Moreover, whenever in the detail view the quantity of a product is updated, then all configured
modifiers are ran for that item. This allows the ItemModelSerializer
, to even change the unit
price of a product, depending on the total content of the cart.
Cart modifiers are easy to write and they normally consist only of a few lines of code. It is the intention of django-SHOP to seed an eco-system for these kinds of plugins. Besides computing the total, cart modifiers can also be used to sum up the weight, if the merchant’s product models specifies it.
Here is an incomplete list of some useful cart modifiers:
Generic Cart Modifiers¶
These kinds of cart modifiers are applied unconditionally onto the cart. A typical instance is the
DefaultCartModifier
, the CartIncludeTaxModifier
or the CartExcludeTaxModifier
.
DefaultCartModifier¶
The shop.modifiers.default.DefaultCartModifier
is required for almost every shopping cart.
It handles the most basic calculations, ie. multiplying the items unit prices with the chosen
quantity. Since this modifier sets the cart item’s line total, it must be listed as the first entry
in SHOP_CART_MODIFIERS
.
Payment Cart Modifier¶
From these kinds of modifiers, only that for the chosen payment method is applied. Payment Modifiers
are used to add extra costs or discounts depending on the chosen payment method. By overriding the
method is_disabled
a payment method can be disabled; useful to disable certain payments in case
the carts total is below a certain threshold.
Shipping Cart Modifier¶
From these kinds of modifiers, only that for the chosen shipping method is applied. Shipping
Modifiers are used to add extra costs or discounts depending on chosen shipping method, the number
of items in the cart and their weight. By overriding the method is_disabled
a shipping method
can be disabled; useful to disable certain payments in case the cart’s total is below a certain
threshold or the weight is too high.
How Modifiers work¶
Cart modifiers should extend the shop.modifiers.base.BaseCartModifier
class and extend one
or more of the given methods:
Note
Until version 0.2 of django-SHOP, the Cart Modifiers returned the amount and label for the extra item rows, and django-SHOP added them up. Since Version 0.3 cart modifiers must change the line subtotals and cart total themselves.
-
class
shop.modifiers.base.
BaseCartModifier
(identifier=None)¶ Cart Modifiers are the cart’s counterpart to backends.
It allows to implement taxes and rebates / bulk prices in an elegant and reusable manner: Every time the cart is refreshed (via it’s update() method), the cart will call all subclasses of this modifier class registered with their full path in settings.SHOP_CART_MODIFIERS.
The methods defined here are called in the following sequence: 1. pre_process_cart: Totals are not computed, the cart is “rough”: only relations and quantities are available 1a. pre_process_cart_item: Line totals are not computed, the cart and its items are “rough”: only relations and quantities are available 2. process_cart_item: Called for each cart_item in the cart. The modifier may change the amount in cart_item.line_total. 2a. add_extra_cart_item_row: It optionally adds an object of type ExtraCartRow to the current cart item. This object adds additional information displayed on each cart items line. 3. process_cart: Called once for the whole cart. Here, all fields relative to cart items are filled. Here the carts subtotal is used to computer the carts total. 3a. add_extra_cart_row: It optionally adds an object of type ExtraCartRow to the current cart. This object adds additional information displayed in the carts footer section. 4. post_process_cart: all totals are up-to-date, the cart is ready to be displayed. Any change you make here must be consistent!
Each method accepts the HTTP request object. It shall be used to let implementations determine their prices according to the session, and other request information.
-
arrange_watch_items
(watch_items, request)¶ Arrange all items, which are being watched. Override this method to resort and regroup the returned items.
-
arrange_cart_items
(cart_items, request)¶ Arrange all items, which are intended for the shopping cart. Override this method to resort and regroup the returned items.
-
pre_process_cart
(cart, request)¶ This method will be called before the Cart starts being processed. It shall be used to populate the cart with initial values, but not to compute the cart’s totals.
-
pre_process_cart_item
(cart, item, request)¶ This method will be called for each item before the Cart starts being processed. It shall be used to populate the cart item with initial values, but not to compute the item’s linetotal.
-
process_cart_item
(cart_item, request)¶ This will be called for every line item in the Cart: Line items typically contain: product, unit_price, quantity and a dictionary with extra row information.
If configured, the starting line total for every line (unit price * quantity) is computed by the DefaultCartModifier, which typically is listed as the first modifier. Posterior modifiers can optionally change the cart items line total.
After processing all cart items with all modifiers, these line totals are summed up to form the carts subtotal, which is used by method process_cart.
-
post_process_cart_item
(cart, item, request)¶ This will be called for every line item in the Cart, while finally processing the Cart. It may be used to collect the computed line totals for each modifier.
-
process_cart
(cart, request)¶ This will be called once per Cart, after every line item was treated by method process_cart_item.
The subtotal for the cart is already known, but the total is still unknown. Like for the line items, the total is expected to be calculated by the first cart modifier, which typically is the DefaultCartModifier. Posterior modifiers can optionally change the total and add additional information to the cart using an object of type ExtraCartRow.
-
post_process_cart
(cart, request)¶ This method will be called after the cart was processed in reverse order of the registered cart modifiers. The Cart object is “final” and all the fields are computed. Remember that anything changed at this point should be consistent: If updating the price you should also update all relevant totals (for example).
-
add_extra_cart_item_row
(cart_item, request)¶ Optionally add an ExtraCartRow object to the current cart item.
This allows to add an additional row description to a cart item line. This method optionally utilizes and/or modifies the amount in cart_item.line_total.
-
add_extra_cart_row
(cart, request)¶ Optionally add an ExtraCartRow object to the current cart.
This allows to add an additional row description to the cart. This method optionally utilizes cart.subtotal and/or modifies the amount in cart.total.
-
Payment Providers¶
Payment Providers are simple classes, which create an interface from an external Payment Service Provider (shortcut PSP) to our django-SHOP framework.
Payment Providers must be aggregates of a Payment Cart Modifier. Here the Payment Cart Modifier computes extra fees when selected as a payment method, whereas our Payment Provider class, handles the communication with the configured PSP, whenever the customer submits the purchase request.
In django-SHOP Payment Providers normally are packed into separate plugins, so here we will show how to create one yourself instead of explaining the configuration of an existing Payment gateway.
A precautionary measure during payments with credit cards is, that the used e-commerce implementation never sees the card numbers or any other sensible information. Otherwise those merchants would have to be PCI-DSS certified, which is an additional, but often unnecessary bureaucratic task, since most PSPs handle that task for us.
Checkout Forms¶
Since the merchant is not allowed to “see” sensitive credit card information, some Payment Service Providers require, that customers are redirected to their site so that there, they can enter their credit card numbers. This for some customers is disturbing, because they visually leave the current shop site.
Therefore other PSPs allow to create form elements in HTML, whose content is send to their site during the purchase task. This can be done using a POST submission, followed by a redirection back to the client. Other providers use Javascript for submission and return a payment token to the customer, who himself forwards that token to the shopping site.
All in all, there are so many different ways to pay, that it is quite tricky to find a generic solution compatible for all of them.
Here django-SHOP uses some Javascript during the purchase operation. Lets explain how:
The Purchasing Operation¶
During checkout, the clients final step is to click onto a button labeled something like “Buy Now”.
This button belongs to an AngularJS controller, provided by the directive shop-dialog-proceed
.
It may look similar to this:
.. code-block:: html
<button shop-dialog-proceed ng-click=”proceedWith(‘PURCHASE_NOW’)” class=”btn btn-success”>Buy Now</button>
Whenever the customer clicks onto that button, the function proceedWith('PURCHASE_NOW')
is
invoked in the scope of the AngularJS controller, belonging to the given directive.
This function first uploads the current checkout forms to the server. There they are validated, and
if everything is OK, an updated checkout context is send back to the client. See
shop.views.checkout.CheckoutViewSet.upload()
for details.
Next, the success handler of the previous submission looks at the given action. In proceedWith
,
we used the magic keyword PURCHASE_NOW
, which starts a second submission to the server,
requesting to begin with the purchase operation (See shop.views.checkout.CheckoutViewSet.purchase()
for details.). This method determines he payment provider previously chosen by the customer. It
then invokes the method get_payment_request()
of that provider, which returns a Javascript
expression.
On the client, this returned Javascript expression is passed to the eval() function and executed; it then normally starts to submit the payment request, sending all credit card data to the given PSP.
While processing the payment, PSPs usually need to communicate with the shop framework, in order to
inform us about success or failure of the payment. To communicate with us, they may need a few
endpoints. Each Payment provider may override the method get_urls()
returning a list of
urlpatterns, which then is used by the Django URL resolving engine.
class MyPSP(PaymentProvider):
namespace = 'my-psp-payment'
def get_urls(self):
urlpatterns = [
url(r'^success$', self.success_view, name='success'),
url(r'^failure$', self.failure_view, name='failure'),
]
return urlpatterns
def get_payment_request(self, cart, request):
js_expression = 'scope.charge().then(function(response) { $window.location.href=response.data.thank_you_url; });'
return js_expression
@classmethod
def success_view(cls, request):
# approve payment using request data returned by PSP
cart = CartModel.objects.get_from_request(request)
order = OrderModel.objects.create_from_cart(cart, request)
order.populate_from_cart(cart, request)
order.add_paypal_payment(payment.to_dict())
order.save()
thank_you_url = OrderModel.objects.get_latest_url()
return HttpResponseRedirect(thank_you_url)
@classmethod
def failure_view(cls, request):
"""Redirect onto an URL informing the customer about a failed payment"""
cancel_url = Page.objects.public().get(reverse_id='cancel-payment').get_absolute_url()
return HttpResponseRedirect(cancel_url)
Note
The directive shop-dialog-proceed
evaluates the returned Javascript expression inside
a chained then(...)
-handler from the AngularJS promise framework. This means that such a
function may itself return a new promise, which is resolved by the next then()
-handler.
As we can see in this example, by evaluating arbitrary Javascript on the client, combined with HTTP-handlers for any endpoint, django-SHOP is able to offer an API where adding new Payment Service Providers doesn’t require any special tricks.
Order¶
During checkout, at a certain point the customer has to click on a button named “Purchase Now”. This operation performs quite a few tasks: One of them is to convert the cart with its items into an order. The final task is to reset the cart, which means to remove its content. This operation is atomic and not reversible.
Order Models¶
An order consists of two models classes Order
and OrderItem
, both inheriting from
BaseOrder
and BaseOrderItem
respectively. As with most models in django-SHOP, they are
Deferred Model Pattern, so that inheriting from a base class automatically sets the
foreign keys to the appropriate model. This gives the programmer the flexibility to add as many
fields to the order model, as the merchant requires for his special implementation.
In most use-cases, the default implementation of the order model will do the job. These default
classes can be found at shop.models.defaults.order.Order
and
shop.models.defaults.order_item.OrderItem
. To materialize the default implementation, it
is enough to import
these two files into the merchant’s shop project. Otherwise the programmer
may create his own order implementation inheriting from BaseOrder
and/or BaseOrderItem
.
Note
Assure that the model OrderItem
is imported (and materialized) before model
Product
and classes derived from it.
The order item quantity can not always be represented by natural numbers, therefore this field must
be added to the OrderItem
implementation rather than its base class. Since the quantity is
copied from the cart item to the order item, its field type must must correspond to that of
CartItem.quantity
.
Create an Order from the Cart¶
Whenever the customer performs the purchase operation, the cart object is converted into a new order object by invoking:
from shop.models.order import OrderModel
order = OrderModel.objects.create_from_cart(cart, request)
order.populate_from_cart(cart, request)
This invocation of order.populate_from_cart
operation is atomic and can take some time. It
normally is performed by the payment provider, whenever a successful payment was received.
Since the merchant’s implementation of Cart
, CartItem
, Order
and OrderItem
may
contain extra fields the shop framework isn’t aware of, the content of these fields also shall be
transferred, whenever a cart is converted into an order object, during the purchasing operation.
If required, the merchant’s implementation of Order
shall override the method
populate_from_cart(cart, request)
, which provides a hook to copy those extra fields from the
cart object to the order object.
Similarly the merchant’s implementation of OrderItem
shall override the method
populate_from_cart_item(cart_item, request)
, which provides a hook to copy those extra fields
from the cart item to the order item object.
Order Numbers¶
In commerce it is mandatory that orders are numbered using a unique and continuously increasing sequence. Each merchant has his own way to generate this sequence numbers and in some implementations it may even come from an external generator, such as an ERP system. Therefore django-SHOP does not impose any numbering scheme for the orders. This intentionally is left over to the merchant’s implementation, which may be implemented as:
from django.db import models
from django.utils.datetime_safe import datetime
from shop.models import order
class Order(order.BaseOrder):
number = models.PositiveIntegerField("Order Number", null=True, default=None, unique=True)
def get_or_assign_number(self):
if self.number is None:
epoch = datetime.now().date()
epoch = epoch.replace(epoch.year, 1, 1)
qs = Order.objects.filter(number__isnull=False, created_at__gt=epoch)
qs = qs.aggregate(models.Max('number'))
try:
epoc_number = int(str(qs['number__max'])[4:]) + 1
self.number = int('{0}{1:05d}'.format(epoch.year, epoc_number))
except (KeyError, ValueError):
# the first order this year
self.number = int('{0}00001'.format(epoch.year))
return self.get_number()
def get_number(self):
return '{0}-{1}'.format(str(self.number)[:4], str(self.number)[4:])
@classmethod
def resolve_number(cls, number):
number = number[:4] + number[5:]
return dict(number=number)
Here we override these three methods, otherwise the order number would be identical to its primary key which is not suitable for all e-commerce sites.
Method get_or_assign_number()
¶
Is used to assign a new number to an Order objects, if none has been assigned yet, otherwise it returns the assigned one.
Method get_number()
¶
Retrieves the order number assigned to an order in a human readable form. Here the first four digits specify the year in which the order was generated, whereas the last five digits are a continuous increasing sequence.
Classmethod resolve_number(number)
¶
Chances are high that we use the order number as slug, or for any other similar identification
purpose. If we look up for a certain order object using Order.objects.get(...)
or
Order.objects.filter(...)
, then we might want to use a number previously retrieved with
get_number
. This classmethod therefore must reverse the operation of building order numbers.
Order Views¶
Displaying the last or former orders in django-SHOP is as simple, as adding two pages to the CMS. Change into the Django admin backend and enter into the CMS page tree. At an appropriate location in that tree add a new page. As page title use “My Orders”, “Ihre Bestellungen”, “Mis Pedidos”, or whatever is appropriate in the natural language used for that site. Multilingual CMS installations offer a page title for each language.
In the CMS page editor click onto the link named Advanced Settings at the bottom of the popup window. As template, choose the default one, provided it contains at least one big placeholder.
Enter “shop-order” into the Id-field just below. This identifier is required by some templates which link directly onto the orders list view page. If this field is not set, some links onto this page might not work properly.
The Order Views must be rendered by their own CMS apphook. Locate the field Application and choose “View Orders”.
Below this “My Orders” page, add another page named “Thanks for Your Order”, “Danke für Ihre Bestellung” or “Gracias por su pedido”. Change into the Advanced Settings view and as the rendering template select “Inherit the template of the nearest ancestor”. Next enter “shop-order-last” into the Id-field just below. As Application choose again “View Orders”.
CMS Apphook for the Order¶
The apphook for the Order View must be provided by the Django project. This is a simple snippet of
boilerplate which has to be added to the merchant’s implementation of the file
myshop/cms_apps.py
:
from cms.apphook_pool import apphook_pool
from shop.cms_apphooks import OrderCMSApp
class OrderApp(OrderCMSApp):
pass
apphook_pool.register(OrderApp)
This apphook uses the class shop.views.order.OrderView
to render the order’s list- and
detail views using the serializers shop.serializers.order.OrderListSerializer
and
shop.serializers.order.OrderDetailSerializer
. Sometimes these defaults aren’t enough and
must be extended by a customized serializer. Say, our Order class contains the rendered
shipping and billing addresses. Then we can extend our serializer class by adding them:
from shop.serializers.order import OrderDetailSerializer
class CustomOrderSerializer(OrderDetailSerializer):
shipping_address_text = serializers.CharField(read_only=True)
billing_address_text = serializers.CharField(read_only=True)
We now can replace the urls
attribute in our apphook class with, say ['myshop.urls.order']
and exchange the default serializer with our customized one:
from django.conf.urls import url
from shop.views.order import OrderView
from myshop.serializers import CustomOrderSerializer
urlpatterns = [
url(r'^$', OrderView.as_view()),
url(r'^(?P<pk>\d+)$', OrderView.as_view(many=False,
detail_serializer_class=CustomOrderSerializer)),
]
Now, when invoking the order detail page appending ?format=api
to the URL, then two new fields,
shipping_address_text
and billing_address_text
shall appear in our context.
Add the Order list view via CMS-Cascade Plugin¶
Click onto View on site and change into front-end editing mode to use the grid-system of djangocms-cascade. Locate the main placeholder and add a Row followed by at least one Column plugin; both can be found in section Bootstrap. Below that column plugin, add a child named Order Views from section Shop.
We have to perform this operation a second time for the page named “Thanks for Your Order”. The context menus for copying and pasting may be helpful here.
Note that the page “My Orders” handles two views: By invoking it as a normal CMS page, it renders a list of all orders the currently logged in customer has purchased at this shop:
Clicking on one of the orders in this list, changes into a detail view, where one can see a list of items purchased during that shopping session:
The rendered list is a historical snapshot of the cart in the moment of purchase. If in the meantime the prices of products, tax rates, shipping costs or whatever changed, then that order object always keeps the values at that time in history. This even applies to translations. Strings are translated into their natural language on the moment of purchase. Therefore the labels added to the last rows of the cart, always are rendered in the language which was used during the checkout process.
Render templates¶
The path of the templates used to render the order views is constructed using the following rules:
- Look for a folder named according to the project’s name, ie.
settings.SHOP_APP_LABEL
in lower case. If no such folder can be found, then use the folder namedshop
. - Search for a subfolder named
order
. - Search for a template named
list.html
ordetail.html
.
These templates are written to be easily extensible by the customized templates. To override them,
add a template with the path, say myshop/order/list.html
to the projects template folder.
Order Workflows¶
Order Workflows are simple plugins that allow the merchant to define rules in a programmatic way, which actions to perform, whenever a certain event happened. A typical event is the confirmation of a payment, which itself triggers further actions, say to print a delivery note.
Instead of implementing each possible combination for all of these use cases, the django-SHOP framework offers a Finite State Machine, where only selected state transition can be marked as possible. These transition further can trigger other events themselves. This prevents to accidently perform invalid actions such as fulfilling orders, which haven’t been paid yet.
In class shop.models.order.BaseOrder
contains an attribute status
which is of type
FSMField
. In practice this is a char-field, which can hold preconfigured states, but which
can not be changed by program code. Instead, by calling specially decorated class methods, this
state then changes from one or more allowed source states into one predefined target state. We
denote this as a state transition.
An incomplete example:
class Order(models.Model):
# other attributes
@transition(field=status, source='new', target='created')
def populate_from_cart(self, cart, request):
# perform some side effects ...
Whenever an Order
object is initialized, its status
is new and is not yet populated
with cart items, meaning that it resides in a pending state. As we have seen earlier, this object
must be populated from the cart. If this succeeds, the status
of our new Order
object
switches to created.
In django-SHOP the merchant can add as many payment providers he wants. This is done in
settings.py
through the configuration directive SHOP_ORDER_WORKFLOWS
which takes a list of
so called “Order Workflow Mixin” classes. On bootstrapping the application and constructing the
Order
class, it additionally inherits from these mixin classes. This gives the merchant an easy
to configure, yet very powerful tool to model the selling process of his e-commerce site according
to his needs. Say, we want to accept bank transfer in advance, so we must add
'shop.payment.defaults.PayInAdvanceWorkflowMixin'
to our configuration setting. Additionally we
must assure that the checkout process has been configured to offer the corresponding cart modifier:
SHOP_CART_MODIFIERS = (
...
'shop.modifiers.defaults.PayInAdvanceModifier',
...
)
This mixin class contains a few transition methods, lets for instance have a closer look onto
@transition(field='status', source=['created'], target='awaiting_payment')
def awaiting_payment(self):
"""Signals that an Order awaits payments."""
This method actually does nothing, beside changing the status from “created” to
“awaiting_payment”. It is invoked by the method get_payment_request()
from
ForwardFundPayment
, which is the default payment provider of the configured
PayInAdvanceModifier
cart modifier.
The class PayInAdvanceWorkflowMixin
has two other transition methods worth mentioning:
@transition(field='status', source=['awaiting_payment'],
target='prepayment_deposited', conditions=[is_fully_paid],
custom=dict(admin=True, button_name=_("Mark as Paid")))
def prepayment_fully_deposited(self):
"""Signals that the current Order received a payment."""
This method can be invoked by the Django admin backend when saving an existing Order object, but
only under the condition that it is fully paid. The method is_fully_paid()
iterates over all
payments associated with its Order object, sums them up and compares them against the total. If the
entered payment equals or exceeds the order’s total, this method returns True
and the condition
for the given transition is met. This then adds a button labeled “Mark as Paid” at the bottom of
the admin view. Whenever the merchant clicks on this button, the above method
prepayment_fully_deposited
is invoked. This then changes the order’s status from
“awaiting_payment” to “prepayment_deposited”. The Notifications of
django-SHOP can intercept this transition change and perform preconfigured action, such as
sending a payment confirmation email to the customer.
Now that the order has been paid, it time to fulfill it. For this a merchant can use the workflow
mixin class shop.shipping.defaults.CommissionGoodsWorkflowMixin
, which gives him a
hand to keep track on the fulfillment of each order. Since this class doesn’t know anything
about an order status of “prepayment_deposited” (this is a private definition of the class
PayInAdvanceWorkflowMixin
), django-SHOP provides a status to mark the payment of an order as
confirmed. Therefore another transition is added to our mixin class, which is invoked automatically
by the framework whenever the status changes to “prepayment_deposited”:
@transition(field='status', source=['prepayment_deposited',
'no_payment_required'], custom=dict(auto=True))
def acknowledge_prepayment(self):
"""Acknowledge the payment."""
self.acknowledge_payment()
This status, “payment_confirmed”, is known by all other workflow mixin classes and must be used as the source argument for their transition methods.
For further details on Finite State Machine transitions, please refer to the FSM docs. This
however does not cover the contents of dictionary custom
. One of the attributes in custom
is button="Any Label"
as explained in the FSM admin docs. The other is auto=True
and has been introduced by django-SHOP itself. It is used to automatically proceed from
one target to another one, without manual intervention, such as clicking onto a button.
Signals¶
Each state transition emits a signal before and after performing the status change. These signals,
pre_transition
and post_transition
can be received by any registered signal handler. In
django-SHOP, the notification framework listens for these events and creates appropriate
notification e-mails, if configured.
But sometimes simple notifications are not enough, and the merchant’s implementation must perform actions in a programmatic way. This for instance could be a query, which shall be sent to the goods management database, whenever a payment has been confirmed successfully.
In Django, we typically register signal handlers in the ready
method of the merchant’s
application configuration:
from django.apps import AppConfig
class MyShopConfig(AppConfig):
name = 'my_shop'
def ready(self):
from django_fsm.signals import post_transition
post_transition.connect(order_event_notification)
def order_event_notification(sender, instance=None, target=None, **kwargs):
if target == 'payment_confirmed':
# do whatever appropriate
In the above order event notification, use instance
to access the corresponding Order
object.
Finite State Machine Diagram¶
If graphviz is installed on the operating system, it is pretty simple to render a graphical representation of the currently configured Finite State Machine. Simply invoke:
./manage.py ./manage.py graph_transitions -o fsm-graph.png
Applied to our demo shop, this gives the following graph:
Order Admin¶
The order admin backend is likely the most heavily used editor for django-SHOP installation. Here the merchant must manage all incoming orders, payments, customer annotations, deliveries, etc. By automating common tasks, the backend shall prevent careless mistakes: It should for instance neither be possible to ship unpaid goods, nor to cancel a delivered order.
Since the django-SHOP framework does not know which class model is used to implement an
Order
, it intentionally doesn’t register its prepared administration class for that model.
This has to be done by the merchant implementing the shop. It allows to add additional fields and
other mixin classes, before registration.
For instance, the admin class used to manage the Order
model in our shop project, could be
implemented as:
from django.contrib import admin
from shop.models.order import OrderModel
from shop.admin.order import (PrintOrderAdminMixin,
BaseOrderAdmin, OrderPaymentInline, OrderItemInline)
@admin.register(OrderModel)
class OrderAdmin(PrintOrderAdminMixin, BaseOrderAdmin):
fields = BaseOrderAdmin.fields + (
('shipping_address_text', 'billing_address_text',),)
inlines = (OrderItemInline, OrderPaymentInline,)
The fields shipping_address_text
and billing_address_text
are not part of the abstract model
class BaseOrder
and therefore must be referenced separately.
Another useful mixin class to be added to this admin backend is PrintOrderAdminMixin
. Whenever
the status of an order is set to “Pick the Goods” a button labeled “Print Delivery Note” is
added to the order admin form. Clicking on that button displays one ore more pages optimized for
printing.
On the other hand, when the status of an order is set to “Pack the Goods” a button labeled “Print Invoice” is added to the order admin form.
The template for the invoice and delivery note can easily be adopted to the corporate design using plain HTML and CSS.
Rendering extra fields¶
The models Order
and OrderItems
both contain a JSON fiels to hold arbitary data, collected
during the checkout process. Here for instance, django-SHOP stores the computations as performed
by the Cart Modifiers. Displaying them in Django’s admin backend would result in
a rendered Python dictionary, which is not well readable by humans.
Therefore the merchant may add a template, which is rendered using the content of that JSON field,
named extra
. For the implemented order model the merchant may add a template named
<myshop>/admin/order-extra.html
to its template folder. This template then shall render all the
fields as available inside that JSON field. Here rows
contains a list of computations added
by the cart modifiers.
Additionally, a merchant may add templates which are rendered using the contents of the JSON fields,
for each of the order item associated with the given order. Since order items can refer to different
types of products, we may add a template for each of them. It is named
<myshop>/admin/orderitem-<productname>-extra.html
whereas productname is the class name in
lowercase of the model implementing that product. If no such template could be found, then a
template named <myshop>/admin/orderitem-product-extra.html
is used as fallback. If no template
is provided, then the content of these extra fields is not rendered.
Re-adding an Order to the Cart¶
Sometimes it can be useful to re-add the content of an order back to the cart. This functionality
currently is implemented only via the REST-API. By checking the field reorder
before posting
the data, the content of the given order is copyied into the cart.
Managing the Deliver Process¶
Depending on the merchant’s setup, an order can be considered as one inseparably unit, or if partial shipping shall be allowed, as a collection of single products, which can be delivered individually.
To enable partial shipping, assure the instantiation of both classes
shop.models.delivery.BaseDelivery
and shop.models.delivery.BaseDeliveryItem
. The
easiest way to do this is to import the materialized classes into an existing model class:
from shop.models.defaults.delivery import Delivery, DeliveryItem
Partial Delivery Workflow¶
The class implementing the Order
, requires additional methods provided by the mixin class
shop.shipping.delivery.PartialDeliveryWorkflowMixin
. Mix this class into the Order
class by configuring
SHOP_ORDER_WORKFLOWS = (
# other workflow mixins
'shop.shipping.defaults.PartialDeliveryWorkflowMixin',
)
Note
Do not combine this mixin with the class CommissionGoodsWorkflowMixin
.
Administration Backend¶
To control partial delivery, add the class shop.admin.delivery.DeliveryOrderAdminMixin
to the amin class class implementing an Order
:
from django.contrib import admin
from shop.admin.order import BaseOrderAdmin
from shop.models.defaults.order import Order
from shop.admin.delivery import DeliveryOrderAdminMixin
@admin.register(Order)
class OrderAdmin(DeliveryOrderAdminMixin, BaseOrderAdmin):
pass
Implementation Details¶
When partial delivery is activated, two additional tables are added to the database, one for each delivery and one for each delivered order item. This allows us to split up the quantity of in ordered item into two or more delivery objects. This can be useful, if a product is sold out, but the merchant wants to ship whatever is available on stock. He then creates a delivery object and assigns the available quantity to each linked delivery item.
If a product is not available at all anymore, the merchant can alternatively cancel that order item.
Designing an Address Model¶
Depending on the merchant’s needs, the business model and the catchment area of the site, the used
address models may vary widely. Since django-SHOP allows to subclass almost every database model,
addresses are no exception here. Therefore the class shop.models.address.BaseAddress
does
not provide any defaults, except for a foreign key to the Customer model and a priority field used
to sort multiple addresses by relevance.
Create a Customized Address Model¶
All the fields which make up an address, such as the addressee, the street name, zip code, etc. are part of the concrete model implementing an address. It is the merchant’s responsibility to define which address fields are required for the site’s needs. Therefore the base address model does not contain any address related fields, they instead have to be declared by the merchant.
A concrete implementation of the shipping address model may look like this, which not really by coincidence is similar to the address model as shipped by default (see below).
from shop.models.address import BaseShippingAddress, ISO_3166_CODES
class ShippingAddress(BaseShippingAddress):
name = models.CharField("Full name", max_length=1024)
address1 = models.CharField("Address line 1", max_length=1024)
address2 = models.CharField("Address line 2", max_length=1024)
zip_code = models.CharField("ZIP / Postal code", max_length=12)
city = models.CharField("City", max_length=1024)
country = models.CharField("Country", max_length=3,
choices=ISO_3166_CODES)
class Meta:
verbose_name = "Shipping Address"
verbose_name_plural = "Shipping Addresses"
Since the billing address may contain different fields, it must be defined separately from the shipping address. To avoid the duplicate definition of common fields for both models, use a mixin class such as:
from django.db import models
from shop.models.address import BaseBillingAddress
class AddressModelMixin(models.Model):
name = models.CharField("Full name"), max_length=1024)
address1 = models.CharField("Address line 1"), max_length=1024)
# other fields
class Meta:
abstract = True
class BillingAddress(BaseBillingAddress, AddressModelMixin):
tax_number = models.CharField("Tax number", max_length=50)
class Meta:
verbose_name = "Billing Address"
verbose_name_plural = "Billing Addresses"
The Default Address Model¶
The simplest way is to materialize the required address classes, is to use them from our default
and convenience models: shop.models.defaults.address.ShippingAddress
and
shop.models.defaults.address.BillingAddress
. Before using them, we check if they
fulfill your requirements. Otherwise we create our own address models inheriting from
shop.models.address.BaseAddress
.
Note
After changing the address model, remember to create a database migration of the merchant implementation, and apply it.
Multiple Addresses¶
In django-SHOP, if the merchant activates this feature, while setting up the site, customers can register more than one address. Using the Checkout Address Form Plugin, we can enable this feature.
Now during checkout, the customer can select one of a previously entered shipping- and billing addresses, or if he desires add a new one to his list of existing addresses.
How Addresses are used¶
Each active Cart
object refers to one shipping address object and/or one billing address
object. This means that the customer can change those addresses whenever he uses the supplied
address forms.
However, when the customer purchases the content of the cart, that address object is converted into
a simple text string and stored inside the then created Order
object. This is to freeze the
actual wording of the entered address. It also assures that the address used for delivery and
printed on the invoice is immune against accidental changes after the purchasing operation.
By adding a template named myshop/address.txt
for both address models, or
myshop/shipping-address.txt
and myshop/billing-address.txt
for each of them, the merchant
can define how the address shall be rendered on fulfilled orders.
Address Formatting¶
Whenever the customer fulfills the purchase operation, the corresponding shipping- and billing address objects are rendered into a short paragraph of plain text, separated by the newline character. This formatted address then is used to print address labels for parcel delivery and printed invoices.
It is the merchant’s responsibility to format these addresses according to the local practice.
A customized address template must be added into the merchant’s implementation below the
templates
folder named myshop/shipping_address.txt
or myshop/billing_address.txt
.
If both address models share the same fields, we may also use myshop/address.txt
as a fallback.
Such an address template may look like:
{{ address.name }}
{{ address.address1 }}{% if address.address2 %}
{{ address.address2 }}
{% endif %}
{{ address.zip_code }} {{ address.city }}
{{ address.get_country_display }}
This template is used by the method as_text()
as found in each address model.
Use Shipping Address for Billing or vice versa¶
Most customers use their shipping address for billing. Therefore, unless you have really special needs, it is suggested to share all address fields required for shipping, also with the billing address. The customer then can reuse the shipping address for billing, if he desires to. Technically, if the billing address is unset, the shipping address is used anyway, but in django-SHOP the merchant has to actively give permission to his customers, to reuse this address for billing.
The merchant has to actively allow this setting on the site, while editing the Address Form Plugin.
Warning
If the merchant allows to use the shipping address for billing and vice versa, then if the customer selects both options, we end up having no address at all. It therefore is strongly recommended, that one address acts as primary, and that the option “Use primary address” is checked only on the secondary one.
Address Forms¶
The address form, where customers can insert their address, is generated automatically and in a DRY
manner. This means that whenever a field is added, modified or removed from the address model, the
corresponding fields in the address input form, reflect those changes and without any additional
programming. When creating the form template, we have to write it using the as_div()
method.
This method also adds automatic client-side form validation to the corresponding HTML code.
Address Form Styling¶
One problem which remains with automatic form generation, is how to style the input fields.
Therefore, django-SHOP wraps every input field into a <div>
-element using a CSS class named
according to the field. This for instance is useful to shorten some input fields and/or place it
onto the same line.
Say, any of our address forms contain the fields zip_code
and location
as shown in the
example above. Then they may be styled as
.shop-address-zip_code {
width: 35%;
display: inline-block;
padding-right: 10px;
}
.shop-address-city {
width: 65%;
display: inline-block;
padding-left: 10px;
}
so that the ZIP field is narrower and precedes the location field on the same line.
Arranging Address Forms¶
Typically, we ask the customer during the checkout process, for his shipping and/or billing addresses. This however is completely up to the merchant; from a technical point of view, the step when to ask the customer for his addresses is completely arbitrary and can be skipped at all for shops which exclusively ship only virtual goods.
Good practice however is, to add the shipping and billing forms on the checkout process. Since we want to ensure that a customer must enter a valid address, we wrap the address forms into a so called Validate Set of Forms Plugin. This inhibits a customer to proceed to the next page and hence to the purchasing step, whenever at least one form did not validate.
Technical Details¶
Each entered and validated shipping- and billing address address is associated with the current cart. This means that the given addresses then are used while fulfilling the purchasing step. Additionally, each address belongs to the customer which entered it. If multiple addresses are enabled, then django-SHOP assigns a priority to each of the entered addresses in ascending order. A customer then can select one of a previously entered address.
Further Reading¶
A good introduction on which fields to use where and when in addresses can be found at http://www.uxmatters.com/mt/archives/2008/06/international-address-fields-in-web-forms.php
Notifications¶
Whenever the status in model Order
changes, the built-in Finite State Machine emits a signal
using Django’s signaling framework. These signals are received by django-SHOP‘s Notification
Framework.
Notification Admin¶
In Django’s admin backend on Start > Shop > Notification, the merchant can configure which email to send to whom, depending on each of the emitted events. When adding or editing a notification, we get a form mask with four input fields:
Notification Identifier¶
An arbitrary name used to distinguish the different notifications. It’s up to the merchant to choose a meaningful name, “Order confirmed, paid with PayPal” could for instance be a good choice.
Event Type¶
Each Order Workflows declares a set of transition targets. For instance, the class
PayInAdvanceWorkflowMixin
declares these targets: “Awaiting a forward fund payment”,
“Prepayment deposited” and “No Payment Required”.
The merchant can attach a notification for each of these transition targets. Here he must choose one from the prepared collection.
The Recipient¶
Transitions events are transmitted for changes in the order status. Each order belongs to one customer, and normally he’s the first one to be informed, if something changes.
But other persons in the context of this e-commerce site might also be interested into a notification. In django-SHOP all staff Users qualify, as it is assumed that they belong to the group eligible to manage the site.
Email Templates¶
From the section Start > Post Office > Email Templates, choose on of the Templates for Emails.
Notification attachments¶
Choose none, one or more static files to be attached to each email. This typically is a PDF with the terms and conditions. We normally want to send them only to our customers, but not to the staff users, otherwise we’d fill up their mail inbox with countless attachments.
Post Office¶
Emails for order confirmations are send asynchronously by django-SHOP. The reason for this is that it sometimes takes a few seconds for an application server to connect via SMTP, and deliver an Email. It is unacceptable to do this synchronously during the most sensitive phase of a purchase operation.
Therefore django-SHOP sends all generated emails using the queuing mail system Post Office. This app can hold a set of different email templates, which use the same template language as Django itself. Emails can be rendered using plain text, HTML or both.
When emails are queued, the chosen template object is stored side by side with its context serialized as JSON. These queued emails are accessible in Django’s admin backend at Start > Post Office > Emails. Their status can either be “queued”, “sent” or “failed”.
As an offline operation, ./manage.py send_queued_mail
renders and sends queued emails to the
given recipient. During this step, the given template is rendered applying the stored context.
Their status then changes to “sent”, or in case of a problem to “failed”.
If django-SHOP is configured to run in a multilingual environment, post office renders the email in the language used during order creation.
Templates for Emails¶
The Message fields can contain any code, which is valid for Django templates. Frequently, a
summary of the order is rendered in these emails, creating a list of ordered items. This list often
is common across all email templates, and therefore it is recommended to prepare it in a base
template for being reused. In the merchants project folder, create those base email templates
inside the folder templates/myshop/email/...
. Then inside the Message fields, these
templates can be loaded and expanded using the well known templatetag
{% extends "myshop/email/somebase.html" %}
Caveats when using an HTML Message¶
Displaying HTML in email clients is a pain. Nobody really can say, which HTML tags are allowed in which client – and there are many email readers out there, far more than Internet browsers.
Therefore when designing HTML templates for emails, one must be really, really conservative.
It may seem anachronistic, but still a best practice is to use the <table>
element, and if
necessary, nest it into their <td>
(tables data) elements. Moreover, use inline styles rather
than a <style>
element containing blocks of CSS. It is recommended to use a special
email framework to avoid nasty quirks, when rendering the templates.
Images can be embedded into HTML emails using two different methods. One is to host the image on the
web-server and to build an absolute URI referring it. Therefore django-SHOP enriches the object
RenderContext
with the base URI for that web-site and stores it as context variable named
ABSOLUTE_BASE_URI
. For privacy reasons, most email clients do not load externally hosted images
by default – the customer then must actively request to load them from the external sources.
Another method for adding images to HTML emails is to inline their payload. This means that images,
instead of referring them by URI, are inlined as a base64-encoded string. Easy-thumbnails offers a
template filter named data_uri
to perform this operation. This of course blows up the overall
size of an email and shall only be used for small an medium sized images.
REST Serializers¶
Good application programming style is to strictly separate of Models, Views and Controllers. In typical classic Django jargon, Views act as, what outsiders normally denote a controller.
Controllers can sometimes be found on the server and sometimes on the client. In django-SHOP a significant portion of the controller code is written in JavaScript in the form of Angular directives.
Therefore, all data exchange between the View and the Model must be performed in a serializable format, namely JSON. This allows us to use the same business logic for the server, as well as for the client. It also means, that we could create native mobile apps, which communicate with a web-application, without ever seeing a line of HTML code.
Moreover, since django-SHOP uses django-CMS to organize all available components, a classic Django “View” does not make much sense anymore. Therefore, as we evolve our Model-View-Control pattern into a modern web application, our REST serializers become the new controllers.
From a Database Model to the Serializer¶
As we already know, all database models from the django-SHOP framework are owned by the merchant implementation. Model serializers reflect their content and hence are tightly coupled with them. We therefore must be able to create our own serializers in a way similar to how we extend our database models. This means that we have a set of base serializers, which perform the task required by their basic counterpart models. Thus, if we extend these models, we normally also might want to extend their serializers.
Every URL is a REST endpoint¶
Every URL which is part of part of django-SHOP, namely the product’s list and detail views, the cart and checkout views, the order list and detail views, they all are REST endpoints. What does that mean?
Catalog List View¶
Say, we are working with the provided demo shop, then the product’s list view is available at
http://localhost:8000/de/shop/ . By appending ?format=json
to the URL, the raw data making up
our product list, is rendered as a JSON object. For humans, this is difficult to read, therefore
the Django Restframework offers a version which is more legible: Instead of the above, we invoke the
URL as http://localhost:8000/de/shop/?format=api . This renders the list of products as:
Catalog Detail View¶
By following a URL of a product’s detail view, say http://localhost:8000/de/shop/smart-phones/apple-iphone-5?format=api , one may check the legible representation such as:
Routing to these endpoints¶
Since we are using CMS pages to display the catalog’s list view, we must provide an apphook which must be attached to this page. Since these catalog apphooks can vary in many ways they are not part of the shop framework, but must be created and added to the project as the Create the CatalogListApp.
Catalog List View¶
The urlpattern matching the regular expression ^$
routes onto the catalog list view class
shop.views.catalog.CMSPageProductListView
passing in a special serializer class, for
example myshop.serializers.ProductSummarySerializer
. This has been customized to represent
our product models in our catalog templates. Since the serialized data now is available as a Python
dictionary or as a plain Javascript object, these templates then can be rendered by the Django
template engine, as well as by the client using for instance AngularJS.
This View class, which inherits from rest_framework.generics.ListAPIView
accepts a list of
filters for restricting the list of items.
As we (ab)use CMS pages as categories, we somehow must assign them to our products. Therefore our
example project assigns a many-to-many field named cms_pages
to our Product model. Using this
field, the merchant can assign each product to one or more CMS pages, using the apphook
Catalog List
.
This special filter_backend
, shop.rest.filters.CMSPagesFilterBackend
, is responsible
for restricting selected products on the current catalog list view.
Catalog Detail View¶
The urlpattern matching the regular expression ^(?P<slug>[\w-]+)$
routes onto the class
shop.views.catalog.ProductRetrieveView
passing in a special serializer class,
myshop.serializers.ProductDetailSerializer
which has been customized to represent our
product model details.
This View class inherits from rest_framework.generics.RetrieveAPIView
. In addition to the
given serializer_class
it can accept these fields:
lookup_field
: Model field to look up for the retrieved product. This defaults toslug
.lookup_url_kwarg
: URL argument as used by the matching RegEx. This defaults toslug
.product_model
: Restrict to products of this type. Defaults toProductModel
.
Add Product to Cart¶
The product detail view requires another serializer, the so called AddToCartSerializer
. This
serializer is responsible for controlling the number of items being added to the cart and gives
feedback on the subtotal of that potential cart item.
By appending the special string add-to-cart
to the URL of a product’s detail view, say
http://localhost:8000/de/shop/smart-phones/apple-iphone-5/add-to-cart?format=api , one may check
the legible representation of this serializer:
This serializer is slightly different than the previous ones, because it not only serializes
data and sends it from the server to the client, but it also deserializes data submitted from the
client back to the server using a post-request. This normally is the quantity, but in more
elaborated use cases, it also could contain attributes to distinguish product variations. The
AddSmartPhoneToCartSerializer
for example, uses this pattern.
Since we may create our own Add this Product to Cart Serializer for each product type in our shop, hence overriding its functionality with a customized implementation, such a serializer may return any other information relevant to the customer. This could for instance be a rebate or just an update of the availability.
Cart and Checkout Views¶
CMS pages containing forms to edit the cart and the checkout views, do not require any URL routing, because their HTML is rendered by the CMS plugin system, whereas form submissions are handled by hard coded REST endpoints. These URLs are exclusively used by Ajax requests and never visible in the URL line of our browser. Those endpoints are configured by adding them to the root resolver at a project level:
urlpatterns = [
...
url(r'^shop/', include('shop.urls', namespace='shop')),
...
]
The serializers of the cart then can be accessed at http://localhost:8000/shop/api/cart/ , those of the watch-list at http://localhost:8000/shop/api/watch/ and those handling the various checkout forms at http://localhost:8000/shop/api/checkout/ . Accessing these URLs can be useful, specially when debugging JavaScript code.
Order List and Detail Views¶
The Order List and Detail Views must be accessible through a CMS page, therefore we need a speaking
URL. This is similar to the Catalog List View. This means that the Order Views require the apphook
named “View Orders”, which must be configured in the advanced settings of the Order’s CMS pages.
This apphook is shipped with django-SHOP itself and can be found at shop/cms_apps.py
.
As with all other Views used by django-SHOP, the content of this View can also be rendered in
its dictionary structure, instead of HTML. Just append ?format=api
to the URL and get the Order
details. In our myshop example this may look like:
Search Result Views¶
As with the Order View, also the Search Results View is accessible through a CMS page. Say, a
search query directed us to http://localhost:8000/en/search/?q=iphone , then the content of this
query can be made visible by adding &format=api
to this URL and get the results in its
dictionary structure. This is specially useful to test if a customized search serializer returns
the expected results. In our myshop example this may look like:
Final Note¶
In previous versions of django-SHOP, these kinds of controller implementations had to be implemented by customized Django View classes. This programming pattern led to bloated code, because the programmer had to do a case distinction, whether the request was of type GET, POST or some kind of Ajax. Now django-SHOP is shipped with reusable View classes, and the merchant’s implementation must focus exclusively on serializers. This is much easier, because it separates the business logic from the underlying request-response-cycle.
Client Side Framework¶
While Django doesn’t impose any client side framework, django-SHOP has to. Here we have to consider that it is unrealistic to expect that an e-commerce site could operate without any client-side JavaScript. For instance, during checkout the customer must be able to edit the cart interactively. We also might want to offer autocompletion and infinite scroll.
Therefore the authors of django-SHOP have decided to add reusable JavaScript components. Here the most obvious choice would have been jQuery, since it is used by the Django administration backend. However by using jQuery, web designers adopting templates for their django-SHOP implementation would inevitably have to write JavaScript code themselves. In order to prevent this, another popular client-side framework has been chosen: AngularJS.
This means that template designers only have to add shop specific HTML elements. All these directives are provided by the django-SHOP framework. Frontend developers therefore do not have to add or adopt any JavaScript code, except for the initialization.
Note
Since django-SHOP uses REST for every part of the communication, the client side framework can be replaced by whatever appropriate.
Initialize the Application¶
As with any application, also the client side must be initialized. This in AngularJS is done
straight forward. Change the outermost HTML element, which typically is the <html>
tag, to
<html ng-app="myShop">
somewhere in this file, include the JavaScript files required by Angular.
For a better organization of the included files, it is strongly recommended to use django-sekizai as assets manager:
{% load static sekizai_tags %}
{% addtoblock "js" %}<script src="{% static 'node_modules/picturefill/dist/picturefill.min.js' %}" type="text/javascript"></script>{% endaddtoblock %}
{% addtoblock "js" %}<script src="{% static 'node_modules/angular/angular.min.js' %}" type="text/javascript"></script>{% endaddtoblock %}
{% addtoblock "js" %}<script src="{% static 'node_modules/angular-sanitize/angular-sanitize.min.js' %}"></script>{% endaddtoblock %}
{% addtoblock "js" %}<script src="{% static 'node_modules/angular-i18n/angular-locale_de.js' %}"></script>{% endaddtoblock %}
{% addtoblock "js" %}<script src="{% static 'node_modules/angular-animate/angular-animate.min.js' %}"></script>{% endaddtoblock %}
{% addtoblock "js" %}<script src="{% static 'node_modules/angular-messages/angular-messages.min.js' %}"></script>{% endaddtoblock %}
Before the closing </body>
-tag, we then combine those includes and initialize the client side
application. Say, we declare a base template for our project:
{% load djng_tags %}
<body>
...
{% render_block "js" postprocessor "compressor.contrib.sekizai.compress" %}
<script type="text/javascript">
angular.module('myShop', ['ngAnimate', 'ngMessages', 'ngSanitize',
{% render_block "ng-requires" postprocessor "djng.sekizai_processors.module_list" %}
]).config(['$httpProvider', function($httpProvider) {
$httpProvider.defaults.headers.common['X-CSRFToken'] = '{{ csrf_token }}';
$httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
}]).config(['$locationProvider', function($locationProvider) {
$locationProvider.html5Mode(false);
}]){% render_block "ng-config" postprocessor "djng.sekizai_processors.module_config" %};
</script>
</body>
By using Sekizai’s templatetag render_block
inside the initialization and configuration phase
of our Angular application, we can delegate the dependency resolution to template expansion and
inclusion.
For example, the editable cart requires its own Angular module, found in a separate Javascript file. Since we honor the principle of encapsulation, we only want to include and initialize that module if the customer loads the view to alter the cart. Here the template for our editable cart starts with:
{% load static sekizai_tags %}
{% addtoblock "js" %}<script src="{% static 'shop/js/cart.js' %}" type="text/javascript"></script>{% endaddtoblock %}
{% addtoblock "ng-requires" %}django.shop.cart{% endaddtoblock %}
Sekizai then collects the content between these addtoblock
s, and renders them using the
render_block
statements shown above. This concept allows us to delegate dependency resolution
and module initialization to whom it concerns.
Angular Modules¶
The django-SHOP framework declares a bunch of Angular directives and controllers, grouped into
separate modules. All these modules are placed into their own JavaScript files for instance
static/shop/js/auth.js
, static/shop/js/cart.js
, static/shop/js/catalog.js
, etc. and use
a corresponding but unique naming scheme, to avoid conflicts with other third party AngularJS
modules. The naming scheme for these three modules is unsurprisingly: django.shop.auth
,
django.shop.cart
, django.shop.catalog
, etc.
This is where Sekizai’s render_block
templatetag, together with the postprocessor
module_list
becomes useful. We now can manage our AngularJS dependencies:
angular.module('myShop', [/* other dependencies */
{% render_block "ng-requires" postprocessor "djng.sekizai_processors.module_list" %}
])
By adding Sekizai’s render_block
templatetag, together with the postprocessor module_config
,
at the end of our initialization statement, we can add arbitrary configuration code.
angular.module('myShop', [/* module dependencies */]
).{% render_block "ng-config" postprocessor "djng.sekizai_processors.module_config" %};
The templatetags {% render_block "ng-requires" ... %}
and {% render_block "ng-config" ... %}
work, because some other template snippets declare {% addtoblock "ng-requires" ... %}
and/or
{% addtoblock "ng-config" ... %}
. Sekizai then collects these declarations and combines them
in render_block
.
Unless additional client functionality is required, these are the only parts where our project requires us to write JavaScript.
Configuration and Settings¶
The django-SHOP framework itself, requires only a few configuration directives. However, since each e-commerce site built around django-SHOP consists of the merchant’s own project, plus a collection of third party Django apps, here is a summary of mandatory and some optional configuration settings:
Django-SHOP specific settings¶
App Label¶
This label is required internally to configure the name of the database tables used in the merchant’s implementation.
SHOP_APP_LABEL = 'myshop'
There is no default setting.
Site Framework¶
You should always activate Django’s site framework and set a default for
SITE_ID = 1
Alternative User Model¶
Django’s built-in User model lacks a few features required by django-SHOP, mainly the
possibility to use the email address as the login credential. This overridden model is 100% field
compatible to Django’s internal model and even reuses its own database table, namely auth_user
.
AUTH_USER_MODEL = 'email_auth.User'
Since this user model intentionally does not enforce uniqueness on the email address, Django would complain if we do not silence this system check:
SILENCED_SYSTEM_CHECKS = ['auth.W004']
For further information, please refer to the Customer Model documentation.
Authentication Backends¶
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
Currency¶
Unless Money types are specified explicitly, each project requires a default currency:
SHOP_DEFAULT_CURRENCY = 'EUR'
The typical format to render an amount is $ 1.23
, but some merchant may prefer 1.23 USD
.
By using the configuration setting:
SHOP_MONEY_FORMAT = '{symbol} {amount}'
we my specify our own money rendering format, where {symbol}
is €, $, £, etc. and {currency}
is EUR, USD, GBP, etc.
Cart Modifiers¶
Each project requires at least one cart modifier in order to initialize the cart. In most
implementations shop.modifiers.defaults.DefaultCartModifier
is enough, but depending
on the product models, the merchant’s may implement an alternative.
To identify the taxes in the cart, use one of the provided tax modifiers or implement a customized one.
Other modifiers may add extra payment and shipping costs, or rebate the total amount depending on whatever appropriate.
SHOP_CART_MODIFIERS = [
'shop.modifiers.defaults.DefaultCartModifier',
'shop.modifiers.taxes.CartExcludedTaxModifier',
# other modifiers
]
For further information, please refer to the Cart Modifiers documentation.
Installed Django Applications¶
This is a configuration known to work. Special and optional apps are discussed below.
INSTALLED_APPS = [
'django.contrib.auth',
'email_auth',
'polymorphic',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'djangocms_admin_style',
'django.contrib.admin',
'django.contrib.staticfiles',
'django.contrib.sitemaps',
'djangocms_text_ckeditor',
'django_select2',
'cmsplugin_cascade',
'cmsplugin_cascade.clipboard',
'cmsplugin_cascade.sharable',
'cmsplugin_cascade.extra_fields',
'cmsplugin_cascade.segmentation',
'cms_bootstrap3',
'adminsortable2',
'rest_framework',
'rest_framework.authtoken',
'rest_auth',
'django_fsm',
'fsm_admin',
'djng',
'cms',
'menus',
'treebeard',
'compressor',
'sekizai',
'sass_processor',
'django_filters',
'filer',
'easy_thumbnails',
'easy_thumbnails.optimize',
'parler',
'post_office',
'haystack',
'shop',
'my_shop_implementation',
]
email_auth
optional but recommended, overrides the built-in authentification. It must be located afterdjango.contrib.auth
.polymorphic
only required, if the site requires more than one type of product model. It presumes that django-polymorphic is installed.djangocms_text_ckeditor
optionally adds a WYSIWYG HTML editor which integrates well with djangoCMS.django_select2
optionally adds a select field to Django’s admin, with integrated autocompletion. Very useful for addings links to products manually. It presumes that django-select2 is installed.cmsplugin_cascade
adds the functionality to add CMS plugins, as provided by django-SHOP, to arbitrary CMS placeholders. This setting including submodules can be removed, if all templates are created manually.cmsplugin_cascade.clipboard
allows the site administrator to copy a set of plugins in one installation and paste it into the placeholder of another one.cmsplugin_cascade.sharable
allows the site administrator to share a preconfigurable set of plugin attributes into an alias, to be reused by many plugins of the same type.cmsplugin_cascade.extra_fields
allows the site administrator to add arbitrary CSS classes, styles and ID-fields to entitled plugins.cmsplugin_cascade.segmentation
allows to segment a set of plugins into logical units.cms_bootstrap3
adds some templates and templatetags to render Bootstrap 3 styled menus and navigation bars.adminsortable2
allows the site administrator to sort various items in Django’s administration backend.rest_framework
,rest_framework.authtoken
andrest_auth
, required, add the REST functionality to the django-SHOP framework.django_fsm
andfsm_admin
, required, add the Finite State Machine to the django-SHOP framework.djng
only required for installations using AngularJS, which is the recommended JavaScript framework. It adds the interface layer between Django and AngularJS and presumes that django-angular is installed.cms
,menus
andtreebeard
are required if django-SHOP is used in combination with djangoCMS.compressor
, highly recommended. Concatenates and minifies CSS and JavaScript files on production systems. It presumes that django-compressor is installed.sekizai
, highly recommended, allows the template designer to group CSS and JavaScript file includes. It presumes that django-sekizai is installed.sass_processor
, optional but recommended, used to convert SASS into pure CSS together with debugging information. It presumes that django-sass-processor is installed.django_filters
, optionally used to filter products by their attributes using request parameters.filer
, highly recommended, manage your media files in Django. It presumes that django-filer is installed.easy_thumbnails
andeasy_thumbnails.optimize
, highly recommended, handle thumbnail generation and optimization. It presumes that easy-thumbnails is installed.parler
is an optional framework which handles the translation of models fields into other natural languages.post_office
highly recommended. An asynchronous mail delivery application which does not interrupt the request-response cycle when sending mail.haystack
optional, handles the interface between Django and Elasticsearch – a full-text search engine. It presumes a running and available instance of ElasticSearch and that django-haystack and drf-haystack is installed.shop
the django-SHOP framework.my_shop_implementation
replace this by the merchant’s implementation of his shop.
Middleware Classes¶
This is a configuration known to work. Special middleware classes are discussed below.
MIDDLEWARE_CLASSES = (
'djng.middleware.AngularUrlMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'shop.middleware.CustomerMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.gzip.GZipMiddleware',
'shop.middleware.MethodOverrideMiddleware',
'cms.middleware.language.LanguageCookieMiddleware',
'cms.middleware.user.CurrentUserMiddleware',
'cms.middleware.page.CurrentPageMiddleware',
'cms.middleware.toolbar.ToolbarMiddleware',
)
djng.middleware.AngularUrlMiddleware
adds a special router, so that we can use Django’sreverse
function from inside JavaScript. Only required in conjunction with django-angular.shop.middleware.CustomerMiddleware
add the Customer object to each request.shop.middleware.MethodOverrideMiddleware
transforms PUT requests wrapped as POST requests back into the PUT method. This is required for compatibility with some JS frameworks and proxies.
Static Files¶
If compressor
and/or sass_processor
are part of INSTALLED_APPS
, add their finders to
the list of the default STATICFILES_FINDERS
:
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'sass_processor.finders.CssFinder',
'compressor.finders.CompressorFinder',
]
Django-SHOP requires a few third party packages, which are not available from PyPI, they
instead must be installed via npm install
. In order to make these files available to our Django
application, we use the configuration setting:
STATICFILES_DIRS = [
('node_modules', '/path/to/project/node_modules'),
]
Some files installed by npm
are processed by django-sass-processor and hence their path
must be made available:
NODE_MODULES_URL = STATIC_URL + 'node_modules/'
SASS_PROCESSOR_INCLUDE_DIRS = (
os.path.join(PROJECT_ROOT, 'node_modules'),
)
- The string provided by
NODE_MODULES_URL
is used by the special functionget-setting()
in the provided SASS files. SASS_PROCESSOR_INCLUDE_DIRS
extends the list of folders to look for@import ...
statements in the provided SASS files.
Template Context Processors¶
Templates rendered by the django-SHOP framework require some additional objects or configuration settings. Add them to each template using these context processors:
TEMPLATES = [{
...
'OPTIONS': {
'context_processors': (
...
'shop.context_processors.customer',
'shop.context_processors.ng_model_options',
),
},
}]
shop.context_processors.customer
adds the Customer object to the rendering context.
shop.context_processors.ng_model_options
adds the AngularJS specific settings
to the rendering context.
Configure the Order Workflow¶
The ordering workflow can be configured using a list or tuple of mixin classes.
SHOP_ORDER_WORKFLOWS = (
'shop.payment.defaults.PayInAdvanceWorkflowMixin',
'shop.shipping.defaults.CommissionGoodsWorkflowMixin',
# other workflow mixins
)
This prevents to display all transitions configured by the workflow mixins inside the administration backend:
FSM_ADMIN_FORCE_PERMIT = True
Email settings¶
Since django-SHOP communicates with its customers via email, having a working outgoing e-mail service is a fundamental requirement for django-SHOP. Adopt these settings to your configuration. Please remember that e-mail is sent asynchronously via django-post_office.
EMAIL_HOST = 'smtp.example.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'no-reply@example.com'
EMAIL_HOST_PASSWORD = 'smtp-secret-password'
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = 'My Shop <no-reply@example.com>'
EMAIL_REPLY_TO = 'info@example.com'
EMAIL_BACKEND = 'post_office.EmailBackend'
Session Handling¶
For performance reasons it is recommended to use a memory based session store such as Redis, rather than a database or disk based store.
SESSION_ENGINE = 'redis_sessions.session'
SESSION_SAVE_EVERY_REQUEST = True
SESSION_REDIS_PREFIX = 'myshop-session'
SESSION_REDIS_DB = 0
Caching Backend¶
For performance reasons it is recommended to use a memory based cache such as Redis, rather than a disk based store. In comparison to memcached, Redis can invalidate cache entries using keys with wildcards, which is a big advantage in django-SHOP.
CACHES = {
'default': {
'BACKEND': 'redis_cache.RedisCache',
'LOCATION': os.environ.get('REDIS_LOCATION', 'redis://localhost:6379/0'),
'KEY_PREFIX': 'myshop-cache',
},
}
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 3600
CACHE_MIDDLEWARE_KEY_PREFIX = 'myshop-cache'
Internationalisation Support¶
Always localize decimal numbers unless you operate you site in the United States:
USE_L10N = True
These settings for internationalisation are known to work in combination with django-cms and django-parler.
USE_I18N = True
LANGUAGE_CODE = 'en'
LANGUAGES = [
('en', "English"),
('de', "Deutsch"),
]
PARLER_DEFAULT_LANGUAGE = 'en'
PARLER_LANGUAGES = {
1: [
{'code': 'de'},
{'code': 'en'},
],
'default': {
'fallbacks': ['de', 'en'],
},
}
CMS_LANGUAGES = {
'default': {
'fallbacks': ['en', 'de'],
'redirect_on_fallback': True,
'public': True,
'hide_untranslated': False,
},
1: [{
'public': True,
'code': 'en',
'hide_untranslated': False,
'name': 'English',
'redirect_on_fallback': True,
}, {
'public': True,
'code': 'de',
'hide_untranslated': False,
'name': 'Deutsch',
'redirect_on_fallback': True,
},]
}
REST Framework¶
The REST framework requires special settings. We namely must inform it how to serialize our special Money type:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'shop.rest.money.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
),
'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 12,
}
SERIALIZATION_MODULES = {'json': 'shop.money.serializers'}
Since the client side is not allowed to do any price and quantity computations, Decimal values are transferred to the client using strings. This also avoids nasty rounding errors.
COERCE_DECIMAL_TO_STRING = True
Django-CMS and Cascade settings¶
Django-SHOP requires at least one CMS template. Assure that it contains a placeholder able to accept
CMS_TEMPLATES = [
('myshop/pages/default.html', _("Default Page")),
]
CMS_PERMISSION = False
cascade_workarea_glossary = {
'breakpoints': ['xs', 'sm', 'md', 'lg'],
'container_max_widths': {'xs': 750, 'sm': 750, 'md': 970, 'lg': 1170},
'fluid': False,
'media_queries': {
'xs': ['(max-width: 768px)'],
'sm': ['(min-width: 768px)', '(max-width: 992px)'],
'md': ['(min-width: 992px)', '(max-width: 1200px)'],
'lg': ['(min-width: 1200px)'],
},
}
CMS_PLACEHOLDER_CONF = {
'Breadcrumb': {
'plugins': ['BreadcrumbPlugin'],
'parent_classes': {'BreadcrumbPlugin': None},
'glossary': cascade_workarea_glossary,
},
'Commodity Details': {
'plugins': ['BootstrapContainerPlugin', 'BootstrapJumbotronPlugin'],
'parent_classes': {
'BootstrapContainerPlugin': None,
'BootstrapJumbotronPlugin': None,
},
'glossary': cascade_workarea_glossary,
},
'Main Content': {
'plugins': ['BootstrapContainerPlugin', 'BootstrapJumbotronPlugin'],
'parent_classes': {
'BootstrapContainerPlugin': None,
'BootstrapJumbotronPlugin': None,
'TextLinkPlugin': ['TextPlugin', 'AcceptConditionPlugin'],
},
'glossary': cascade_workarea_glossary,
},
'Static Footer': {
'plugins': ['BootstrapContainerPlugin', ],
'parent_classes': {
'BootstrapContainerPlugin': None,
},
'glossary': cascade_workarea_glossary,
},
}
Django-SHOP enriches djangocms-cascade with a few shop specific plugins.
from cmsplugin_cascade.extra_fields.config import PluginExtraFieldsConfig
CMSPLUGIN_CASCADE_PLUGINS = [
'cmsplugin_cascade.segmentation',
'cmsplugin_cascade.generic',
'cmsplugin_cascade.icon',
'cmsplugin_cascade.link',
'shop.cascade',
'cmsplugin_cascade.bootstrap3',
]
CMSPLUGIN_CASCADE = {
'link_plugin_classes': [
'shop.cascade.plugin_base.CatalogLinkPluginBase',
'cmsplugin_cascade.link.plugin_base.LinkElementMixin',
'shop.cascade.plugin_base.CatalogLinkForm',
],
'alien_plugins': ['TextPlugin', 'TextLinkPlugin', 'AcceptConditionPlugin'],
'bootstrap3': {
'template_basedir': 'angular-ui',
},
'plugins_with_sharables': {
'BootstrapImagePlugin': ['image_shapes', 'image_width_responsive', 'image_width_fixed',
'image_height', 'resize_options'],
'BootstrapPicturePlugin': ['image_shapes', 'responsive_heights', 'image_size', 'resize_options'],
},
'bookmark_prefix': '/',
'segmentation_mixins': [
('shop.cascade.segmentation.EmulateCustomerModelMixin', 'shop.cascade.segmentation.EmulateCustomerAdminMixin'),
],
'allow_plugin_hiding': True,
}
Since we want to add arbitrary links onto the detail view of a product, django-SHOP offers
a modified link plugin. This has to be enabled using the 3-tuple link_plugin_classes
.
Django-SHOP uses AngularJS rather than jQuery to control its dynamic HTML widgets.
We therefore have to override the default with this settings:
CMSPLUGIN_CASCADE['bootstrap3']['template_basedir']
.
For a detailed explanation of these configuration settings, please refer to the documentation of djangocms-cascade.
CK Text Editor settings¶
By default, django-CMS uses the CKEditor plugin which can be heavily configured. Settings which have shown to be useful are:
CKEDITOR_SETTINGS_CAPTION = {
'language': '{{ language }}',
'skin': 'moono',
'height': 70,
'toolbar_HTMLField': [
['Undo', 'Redo'],
['Format', 'Styles'],
['Bold', 'Italic', 'Underline', '-', 'Subscript', 'Superscript', '-', 'RemoveFormat'],
['Source']
],
}
CKEDITOR_SETTINGS_DESCRIPTION = {
'language': '{{ language }}',
'skin': 'moono',
'height': 250,
'toolbar_HTMLField': [
['Undo', 'Redo'],
['cmsplugins', '-', 'ShowBlocks'],
['Format', 'Styles'],
['TextColor', 'BGColor', '-', 'PasteText', 'PasteFromWord'],
['Maximize', ''],
'/',
['Bold', 'Italic', 'Underline', '-', 'Subscript', 'Superscript', '-', 'RemoveFormat'],
['JustifyLeft', 'JustifyCenter', 'JustifyRight'],
['HorizontalRule'],
['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Table'],
['Source']
],
}
Media assets handling¶
Django-CMS and django-SHOP rely on django-filer in combination with easy-thumbnails to manage the media assets.
MEDIA_ROOT = '/path/to/project/media'
MEDIA_URL = '/media/'
FILER_ALLOW_REGULAR_USERS_TO_ADD_ROOT_FOLDERS = True
FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880
THUMBNAIL_OPTIMIZE_COMMAND = {
'gif': '/usr/bin/optipng {filename}',
'jpeg': '/usr/bin/jpegoptim {filename}',
'png': '/usr/bin/optipng {filename}'
}
THUMBNAIL_PRESERVE_EXTENSIONS = True
THUMBNAIL_PROCESSORS = [
'easy_thumbnails.processors.colorspace',
'easy_thumbnails.processors.autocrop',
'filer.thumbnail_processors.scale_and_crop_with_subject_location',
'easy_thumbnails.processors.filters',
]
all settings are explained in detail in the documentation of django-filer and easy-thumbnails.
Full Text Search¶
Presuming that you installed and run an ElasticSearchEngine server, configure Haystack:
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
'URL': 'http://localhost:9200/',
'INDEX_NAME': 'my_prefix-en',
},
}
If you want to index other natural language, say German, add another prefix:
HAYSTACK_CONNECTIONS = {
...
'de': {
'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
'URL': 'http://localhost:9200/',
'INDEX_NAME': 'my_prefix-de',
}
}
HAYSTACK_ROUTERS = ('shop.search.routers.LanguageRouter',)
AngularJS specific settings¶
The cart’s totals are updated after an input field has been changed. For usability reasons it makes sense to delay this, so that only after a certain time of inactivity, the update is triggered.
SHOP_ADD2CART_NG_MODEL_OPTIONS = "{updateOn: 'default blur', debounce: {'default': 500, 'blur': 0}}"
This configuration updates the cart after changing the quantity and 500 milliseconds of inactivity or field blurring. It is used by the “Add to cart” form.
SHOP_EDITCART_NG_MODEL_OPTIONS = "{updateOn: 'default blur', debounce: {'default': 2500, 'blur': 0}}"
This configuration updates the cart after changing any of the product’s quantities and 2.5 seconds of inactivity or field blurring. It is used by the “Edit cart” form.
Select2 specific settings¶
django-select2 adds a configurable autocompletion field to the project.
Change the include path to a local directory, if you prefer to install the JavaScript dependencies
via npm
instead of relying on a preconfigured CDN:
SELECT2_CSS = 'node_modules/select2/dist/css/select2.min.css'
SELECT2_JS = 'node_modules/select2/dist/js/select2.min.js'
Shipping Providers¶
Unless you use the merchant management systems for delivery, django-SHOP provides some hooks to add shipping providers. Shipping providers require that the Delivery model is available, otherwise there is no way to keep track which items have been shipped with a delivery.
Special CMS Pages¶
Besides the Catalog-, Cart- and Checkout Views, some pages must be accessed from already prepared
templates, which are shipped with this framework. These templates use the templatetag
{% page_url %}
shipped by djangoCMS with some hard coded IDs. Unless we want to rewrite those
templates, we must provide a few special CMS pages, where we specify those page IDs.
Customer Self Registering Page¶
The django-SHOP framework offers a plugin, which offers a form, where customers can enter their email address and a password. This plugin is named Authentication using the Rendered Form: Register User.
Sometimes
self-registering
- This page shall offer a form,
- This plugin
In the Advanced Settings of the CMS page handling this form, use shop-register-customer
as the page ID.
Customer Details Page¶
This page shall offer a form, where a customer can enter his personal details, such as his or her names, email address and whatever else is interesting for the merchant.
Controlling the Cart Icon¶
On e-commerce sites, typically a cart- or basket symbol is located on the top right corner of the navigation bar and clicking on it, normally loads the cart page.
Together with the cart icon, we often want to display an additional caption, such as the number
of items and/or the cart’s total. The cart item typically is rendered using the templatetag
{% cart_icon %}
. It can be styled using the template myshop/templatetags/cart-icon.html
,
or if it doesn’t exist, falls back on shop/templatetags/cart-icon.html
.
Settings¶
These is the complete list of setting directives available for django-SHOP.
Usage in your own code:
from shop.conf import app_settings
print(app_settings.APP_LABEL)
Note
When using as shown here, you don’t have to prefix the settings property with SHOP_...
.
-
class
shop.conf.
DefaultSettings
¶ -
SHOP_APP_LABEL
¶ The name of the project implementing the shop, for instance
myshop
.This is required to assign the abstract shop models to a project. There is no default.
-
SHOP_DEFAULT_CURRENCY
¶ The default currency this shop is working with. The default is
EUR
.Note
All model- and form input fields can be specified for any other currency, this setting is only used if the supplied currency is missing.
-
SHOP_VENDOR_EMAIL
¶ The vendor’s email addresses, unless specified through the
Order
object.
-
SHOP_MONEY_FORMAT
¶ When rendering an amount of type Money, use this format.
Possible placeholders are:
{symbol}
: This is replaced by €, $, £, etc.{currency}
: This is replaced by EUR, USD, GBP, etc.{amount}
: The localized amount.
-
SHOP_DECIMAL_PLACES
¶ Number of decimal places for the internal representation of a price. This is purely used by the Django admin and is not the number of digits visible by the customer.
Defaults to 2.
-
SHOP_CUSTOMER_SERIALIZER
¶ Depending on the materialized customer model, use this directive to configure the customer serializer.
Defaults to
shop.serializers.defaults.CustomerSerializer
.
-
SHOP_PRODUCT_SUMMARY_SERIALIZER
¶ Serialize the smallest common denominator of all Product models available in this shop. This serialized data then is used for Catalog List Views, Cart List Views and Order List Views.
Defaults to a minimalistic Product serializer.
-
SHOP_PRODUCT_SELECT_SERIALIZER
¶ This serializer is only used by the plugin editors, when selecting a product using a drop down menu with auto-completion.
Defaults to
shop.serializers.defaults.ProductSelectSerializer
.
-
SHOP_ORDER_ITEM_SERIALIZER
¶ Depending on the materialized OrderItem model, use this directive to configure the serializer.
Defaults to
shop.serializers.defaults.OrderItemSerializer
.
-
SHOP_CART_MODIFIERS
¶ Specifies the list of Cart Modifiers. They are are applied on each cart item and the cart final sums.
This list typically starts with
'shop.modifiers.defaults.DefaultCartModifier'
as its first entry, followed by other cart modifiers.
-
SHOP_VALUE_ADDED_TAX
¶ Use this convenience settings if you can apply the same tax rate for all products and you use one of the default tax modifiers
shop.modifiers.taxes.CartIncludeTaxModifier
orshop.modifiers.taxes.CartExcludedTaxModifier
.If your products require individual tax rates or you ship into states with different tax rates, then you must provide your own tax modifier.
-
SHOP_ORDER_WORKFLOWS
¶ Specifies a list of Order Workflows. Order workflows are applied after an order has been created and conduct the vendor through the steps of receiving the payments until fulfilling the shipment.
-
SHOP_ADD2CART_NG_MODEL_OPTIONS
¶ Used to configure the update behavior when changing the quantity of a product, in the product’s detail view after adding it to the cart. For more information refer to the documentation of the NgModelOptions directive in the AngularJS reference.
-
SHOP_EDITCART_NG_MODEL_OPTIONS
¶ Used to configure the update behavior when changing the quantity of a cart item, in the cart’s edit view. For more information refer to the documentation of the NgModelOptions directive in the AngularJS reference.
-
SHOP_GUEST_IS_ACTIVE_USER
¶ If this directive is
True
, customers which declared themselves as guests, may request a password reset, so that they can log into their account at a later time. Then it also makes sense to set theemail
field in modelemail_auth.User
as unique.The default is
False
.
-
SHOP_CACHE_DURATIONS
¶ In the product’s list views, HTML snippets are created for the summary representation of each product.
By default these snippet are cached for one day.
-
SHOP_DIALOG_FORMS
¶ Specify a list of dialog forms available in our
shop.views.checkout.CheckoutViewSet
. This allows us to use its endpointresolve('shop:checkout-upload')
in a generic way.If Cascade plugins are used for the forms in the checkout view, this list can be empty.
-
How To’s¶
Some recipes on how to perform certain tasks in django-SHOP.
This collection of recipes unfortunately is not finished yet.
Add Customized HTML Snippets¶
When working in Structure Mode as provided by djangoCMS, while editing the DOM tree inside a placeholder, we might want to add a HTML snippet which is not part of the Cascade ecosystem. Instead of creating an additional Django template, it often is much easier to just add a customized plugin. This plugin then is available when editing a placeholder in Structure Mode.
Customized Cascade plugin¶
Creating a customized plugin for the merchant’s implementaion of that e-commerce project is very easy. Just add this small Python module:
from cms.plugin_pool import plugin_pool
from shop.cascade.plugin_base import ShopPluginBase
class MySnippetPlugin(ShopPluginBase):
name = "My Snippet"
render_template = 'myshop/cascade/my-snippet.html'
plugin_pool.register_plugin(MySnippetPlugin)
then, in the project’s settings.py
register that plugin together with all other Cascade
plugins:
CMSPLUGIN_CASCADE_PLUGINS = (
'cmsplugin_cascade.segmentation',
'cmsplugin_cascade.generic',
'cmsplugin_cascade.link',
'shop.cascade',
'cmsplugin_cascade.bootstrap3',
'myshop.cascade',
...
)
The template itself myshop/cascade/my-snippet.html
can contain all templatetags as configured
within the Django project.
Often we want to associate customized styles and/or scripts to work with our new template. Since we honor the principle of encapsulation, we somehow must refer to these files in a generic way. This is where django-sekizai helps us:
{% load static sekizai_tags %}
{% addtoblock "css" %}<link href="{% static 'myshop/css/my-snippet.css' %}" rel="stylesheet" type="text/css" />{% endaddtoblock %}
{% addtoblock "js" %}<script src="{% static 'myshop/js/my-snippet.js' %}" type="text/javascript"></script>{% endaddtoblock %}
<div>
my snippet code goes here...
</div>
Note
The main rendering template requires a block such as {% render_block "css" %}
and
{% render_block "js" %}
which then displays the stylesheets and scripts inside the
appropriate HTML elements.
Further customizing the plugin¶
Sometimes we require additional parameters which shall be customizable by the merchant, while editing the plugin. For Cascade this can be achieved very easily. First think about what kind of data to store, and which form widgets are appropriate for that kind of editor. Say we want to add a text field holding the snippets title, then change the change the plugin code from above to:
class MySnippetPlugin(ShopPluginBase):
...
title = GlossaryField(widgets.TextInput(), label=_("Title"))
Inside the rendering template for that plugin, the newly added title can be accessed as:
<h1>{{ instance.glossary.title }}</h1>
<div>...
Cascade offers many more options than just these. For details please check its reference guide.
Creating a customized Form snippet¶
Sometimes we might need a dialog form, to store arbitrary information queried from the customer
using a customized form. Say we need to know, when to deliver the goods. This information will be
stored inside the dictionary Cart.extra
and thus transferred automatically to Order.extra
whenever the cart object is converted into an order object.
Our form plugin now must inherit from shop.cascade.plugin_base.DialogFormPluginBase
instead
of our ordinary shop plugin class:
from cms.plugin_pool import plugin_pool
from shop.models.cart import CartModel
from shop.cascade.plugin_base import DialogFormPluginBase
class DeliveryDatePlugin(DialogFormPluginBase):
name = "Delivery Date"
form_class = 'myshop.forms.DeliveryDateForm'
render_template = 'myshop/checkout/delivery-date.html'
def get_form_data(self, context, instance, placeholder):
cart = CartModel.objects.get_from_request(context['request'])
initial = {'delivery_date': getattr(cart, 'extra', {}).get('delivery_date', '')}
return {'initial': initial}
DialogFormPluginBase.register_plugin(DeliveryDatePlugin)
here additionally we have to specify a form_class
. This form class can inherit from
shop.forms.base.DialogForm
or shop.forms.base.DialogModelForm
. Its behavior is
almost identical to its Django’s counterparts:
class DeliveryDateForm(DialogForm):
scope_prefix = 'data.delivery_date'
date = fields.DateField(label="Delivery date")
@classmethod
def form_factory(cls, request, data, cart):
delivery_date_form = cls(data=data)
if delivery_date_form.is_valid():
cart.extra.update(delivery_date_form.cleaned_data)
return delivery_date_form
The scope_prefix
marks the JavaScript object below our AngularJS $scope
. This must be an
identifier which is unique across all dialog forms building up our ecosystem of Cascade plugins.
The classmethod form_factory
must, as its name implies, create a form object of the class it
belongs to. As in our example from above, we use this to update the cart’s extra
dictionary,
whenever the customer submitted a valid delivery date.
The last piece is to put everything together using a form template such as:
{% extends "shop/checkout/dialog-base.html" %}
{% block dialog_form %}
<form name="{{ delivery_date_form.form_name }}" novalidate>
{{ delivery_date_form.as_div }}
</form>
{% endblock %}
Handling Discounts¶
Generally, this is how you implement a “bulk rebate” module, for instance.
Taxes¶
As a general rule, the unit price of a product, shall always contain the net price. When our
products show up in the catalog, their method get_price(request)
is consulted by the framework.
It is here where you add tax, depending on the tax model to apply. See below.
Use Cart Modifiers to handle Value Added Tax¶
Django-SHOP is not shipped with any kind of built-in tax handling code. This is because tax models vary from product to product and region to region. Therefore the tax computation shall be pluggable and easily exchangeable.
American tax model¶
The American tax model presumes that all prices are shown as net prices, hence the subtotal is the sum of all net prices. On top of the subtotal we add the taxes and hence compute the total.
A simple tax cart modifier which adds the tax on top of the subtotal:
from shop.serializers.cart import ExtraCartRow
from shop.modifiers.base import BaseCartModifier
VALUE_ADDED_TAX = 9.0
class CartIncludeTaxModifier(BaseCartModifier):
taxes = VALUE_ADDED_TAX / 100
def add_extra_cart_row(self, cart, request):
amount = cart.subtotal * self.taxes
instance = {
'label': "plus {}% VAT".format(VALUE_ADDED_TAX),
'amount': amount,
}
cart.extra_rows[self.identifier] = ExtraCartRow(instance)
cart.total += amount
European tax model¶
The European tax model presumes that all prices are shown as gross prices, hence the subtotal already contains the taxes. However, we must report the contained taxes on the invoice.
A simple tax cart modifier which reports the tax already included in the subtotal:
from shop.serializers.cart import ExtraCartRow
from shop.modifiers.base import BaseCartModifier
VALUE_ADDED_TAX = 19.0
class CartExcludedTaxModifier(BaseCartModifier):
taxes = 1 - 1 / (1 + VALUE_ADDED_TAX / 100)
def add_extra_cart_row(self, cart, request):
amount = cart.subtotal * self.taxes
instance = {
'label': "{}% VAT incl.".format(VALUE_ADDED_TAX),
'amount': amount,
}
cart.extra_rows[self.identifier] = ExtraCartRow(instance)
Note that here we do not change the current total.
Mixed tax models¶
When doing business to business, then in Europe the American tax model is used. Sites handling both
private customers as well as business customers must provide a mixture of both tax models.
Since business customers can be identified through the Customer
objects provided by request
object, we can determine which tax model to apply in each situation.
Varying Taxes per Item¶
For certain kind of products, different tax rates must be applied. If your e-commerce site must handle these kinds of products, then we add a tag to our product model. This could be an enum field, with one value per tax rate or a decimal field containing the rate directly.
In this example we use the latter, where each product contains a field named vat
, containing the
tax rate in percent.
from shop.serializers.cart import ExtraCartRow
from shop.modifiers.base import BaseCartModifier
from myshop.models.product import Product
class TaxModifier(BaseCartModifier):
def __init__(self, identifier=None):
super(TaxModifier, self).__init__(identifier)
self.tax_rates = Product.objects.order_by('vat').values('vat').annotate(count=Count('vat'))
def pre_process_cart(self, cart, request):
for rate in self.tax_rates:
tax_attr = '_{}_vat_{vat}'.format(self.identifier, **rate)
setattr(cart, tax_attr, Money(0))
def add_extra_cart_item_row(self, cart_item, request):
vat = cart_item.product.vat
tax_attr = '_{0}_vat_{1}'.format(self.identifier, vat)
amount = cart_item.line_total * Decimal(vat) / 100
setattr(cart_item, tax_attr, amount)
def post_process_cart_item(self, cart, cart_item, request):
tax_attr = '_{0}_vat_{1}'.format(self.identifier, cart_item.product.vat)
setattr(cart, tax_attr, getattr(cart, tax_attr) + getattr(cart_item, tax_attr))
def add_extra_cart_row(self, cart, request):
for rate in self.tax_rates:
tax_attr = '_{}_vat_{vat}'.format(self.identifier, **rate)
instance = {
'label': "plus {vat}% VAT".format(**rate),
'amount': getattr(cart, tax_attr),
}
cart.extra_rows['{}:vat_{vat}'.format(self.identifier, **rate)] = ExtraCartRow(instance)
def process_cart(self, cart, request):
super(TaxModifier, self).process_cart(cart, request)
for rate in self.tax_rates:
tax_attr = '_{}_vat_{vat}'.format(self.identifier, **rate)
cart.total += getattr(cart, tax_attr)
First, in method pre_process_cart
we add additional attributes to the cart object, in order to
have a placeholder where to sum up the taxes for each tax rate.
In method add_extra_cart_item_row
we compute the tax amount for each item individually and store
it as additional attribute in each cart item.
In method post_process_cart_item
we sum up the tax amount over all cart items.
In method add_extra_cart_row
we report the sum of all tax rates individually. They will show up
on the invoice using one line per tax rate.
Finally, in method process_cart
we sum up all tax amounts for all rates and add them to the
cart’s total.
Development and Community¶
Changelog for django-SHOP¶
0.11.1¶
- Fix migration
0007_notification
to handle fieldmail_to
correctly. - Allow transition to cancel order only for special targets.
0.11¶
- Fix:
shop.rest.renderers.CMSPageRenderer
always uses the template offered by the CMS page, rather than invoking methodget_template_names()
from the correspondingAPIView
class. - Feature: Add class:shop.rest.renderers.ShopTemplateHTMLRenderer which is the counterpart of
shop.rest.renderers.CMSPageRenderer
, usable for hardcoded Django views. - Refactor: In examples polymorphic and i18n_polymorphic, renamed
SmartPhone
toSmartPhoneVariant
. - Feature: In
shop.money.fields.MoneyFormField
use a widget which renders the currency. - Refactor: In
shop.money.fields.MoneyField
, drop support for implicit default value, since it causes more trouble than benefit. - Fix: Handle non-decimal types in
shop.money.fields.MoneyField.get_db_prep_save()
. - Fix: In AngularJS, changes on filters and the search field did not work on Safari.
- Fix: In
shop.views.auth.AuthFormsView.post()
create a customer object from request for a visiting customers, rather than responding with BAD REQUEST. - Fix:
shop.models.order.OrderManager.get_summary_url()
only worked for views rendered as CMS page. Now it also works for static Django views. - Simplified all methods
get_urls()
from all classes derived fromCMSApp
by exploiting CMS-PR 5898 introduced with django-CMS-3.4.4. - Remove field
customer
fromshop.serializers.order.OrderListSerializer
, since it interfered with thecustomer
object on the global template_context namespace, causing template shop/navbar/login-logout.html to fail. - Management command
fix_filer_bug_965
is obsolete with django-filer-1.2.8. - Fix: Use caption in Order Detail View.
- Add Leaflet Map plugin from djangocms-cascade for demonstration purpose.
- Moved
package.json
intoexample/package.json
(and with itnode_modules
) since it shall be part of the project, rather than the Django app. - Fix: In
shop.models.order.BaseOrderItem.populate_from_cart_item()
theunit_price
is takes from thecart_item
, rather than beeing recalculated. shop.cascade.cart.ShopCartPlugin
accepts two children:ShopLeftExtension
andShopRightExtension
which can be used to add plugins inside the cart’s table footer.- In
shop.models.notification.Notification
renamed fieldmail_to
torecipient
and converted it to aForeignKey
. Added an enum fieldnotify
to distinguish between different kinds of recipients. - Refactored
CustomerStateField
into a reusableshop.models.fields.ChoiceEnumField
which can be used for both,Notify
as well asCustomerState
. - Adopted to djangocms-cascade version 0.14, which allows to render static pages using plugin descriptions in JSON.
- Added Paginator to Order List View.
- Refactored
shop.app_settings
intoshop.conf.app_settings
to be usable by Sphinx in docstrings. - Added
shop.models.order.BaseOrder.get_all_transitions()
which returns all possible transitions for the the Order class. - In
shop.rest.renderers.ShopTemplateHTMLRenderer
do not pollutetemplate_context
with serialized data on the root level. - Fix #623: Template
auth/register-user.html
did not validate properly, when Reset password was checked. - Added AngularJS filter
range
to emulate enumerations in JavaScript. - Fallback to hard-coded URL if CMS page for “Continue Shopping” is missing.
0.10.2¶
- Fixed migration error in
0004_ckeditor31.py
. - Fixed #554: Email is no longer created when notification is triggered.
- Fixed: Using a
ManyToManyField
throughProductPage
ignores the blank attribute, when saving a product in the admin backend. - Hard code “Cart” into tooltip for cart icon, until https://github.com/divio/django-cms/issues/5930 is fixed.
- Renders a nicer summary when rendering a multiple address form.
- Fixed: When placeholder is
None
raises AttributeError.
0.10.1¶
- Fixed #537 and #539: Rendering data in template has different results after upgrading to 0.10.
0.10.0¶
- In the backend,
OrderAdmin
andOrderItemAdmin
may render the dictionaryextra
from their associated models using a special template. - In
OrderAdmin
use methodsget_fields()
andget_readonly_fields()
as intended. - Tested with Django-1.10. Drop support for Django-1.8.
- If an anonymous customer logs in, his current cart is merged with a cart, which has previously been created. This has been adopted to re-use the method Product.is_in_cart() in and finds it’s Merge the contents of the other cart into this one, afterwards delete it.
- Moved field
salutation
fromshop.models.customer.BaseCustomer
into the merchant implementation. If your project does not use the provided default customer modelshop.models.defaults.customer.Customer
, then you should add thesalutation
field to your implementation of the Customer model, if that makes sense in your use-case. - Refactored the defaults settings for
shop
using anAppSettings
object. - Refactored all serializers into their own folder
shop/serializers
with submodulesbases.py
,cart.py
,order.py
anddefaults.py
. The serializersCustomerSerializer
,ProductSummarySerializer
andOrderItemSerializer
now are configurable through the application settings. - AngularJS directive
<shop-auth-form ...>
now listens of the event “pressed ENTER key” and submits the form data accordingly. - Upgraded to AngularJS version 1.5.9.
- HTML5 mode is the default now.
- The previously required additional endpoint for the autocomplete search, can now be be merged
into the same endpoint as connected to the catalog’s list view. This has been made possible by
the wrapper
shop.search.views.CMSPageCatalogWrapper
which dispatch incoming requests to either theshop.views.catalog.ProductListView
or, for search queries toshop.search.views.SearchView
. - Added choice option “Infinite Scroll” to the Cascade plugins Catalog List View and Search Results. They influence if the paginator is rendered or trigger an event to load more results from the server.
- Changed all Cascade plugins to follow the new API introduced in djangocms-cascade version 0.12.
- Directive
shop-product-filter
must be member of a<form ...>
element. - Unified the plugins ShippingAddressFormPlugin and BillingAddressFormPlugin into one plugin named CheckoutAddressPlugin, where the merchant can choose between the shipping- or billing form.
- Refactored
shop.forms.checkout.AddressForm
and fixed minor bugs when editing multiple addresses. - In address models, replaced
CharField
forcountry
against a specialCountryField
. - Change value of
BaseShippingAddress.address_type
toshipping
andBaseBillingAddress.address_type
tobilling
. - Method
shop.models.order.OrderManager.get_latest_url()
falls back toreverse('shop-order-last')
if no such page with IDshop-order-last
was found in the CMS. - Use menu_title instead of page title for link and tooltip content.
- In
DialogForm
, fieldplugin_id
is not required anymore. - After a new customer recognized himself, the signal
customer_recognized
is fired so that other apps can act upon. - Unified
ProductCommonSerializer
,ProductSummarySerializer
andProductDetailSerializer
into a singleProductSerializer
, which acts as default for theProductListView
andProductRetrieveView
. - Dependency to djangocms-cascade is optional now.
- Added alternative compressor for
{% render_block "js/css" "shop.sekizai_processors.compress" %}
which can handle JS/CSS files provided using{% addtoblock "js/css" ... %}
even if located outside the/static/
folders. - Added method
post_process_cart_item
to the Cart Modifiers. - In
CartItem
theproduct_code
is mandatory now. It moves from being optionally kept in dictCartItem.extra
into theCartItem
model itself. This simplifies a lot of boilerplate code, otherwise required by the merchant implementation. Please read Upgrading to 0.10.0 for details. - In
shop.models.product.BaseProduct
added a hook methodget_product_variant(self, **kwargs)
which can be overridden by products with variations to return a product variant.
0.9.3¶
- Added template context processor
shop.context_processors.ng_model_options()
to add the settingsEDITCART_NG_MODEL_OPTIONS
andADD2CART_NG_MODEL_OPTIONS
. Please check your templates to see, if you still useng_model_options
. - Allows to add children to the
CartPlugin
. These children are added to the table foot of the rendered cart. - Added AngularJS directive
<ANY shop-forms-set>
which can be used as a wrapper, when the proceed button shall be added to a page containing<form ...>
elements with built in validation. - All Cascade plugins use
GlossaryField
instead of a list ofPartialFormField
s. This is much more “Djangonic”, but requires djangocms-cascade version 0.11 or later. - All urlpatterns are compatible with configurations adding a final / to the request URL.
- The URL for accessing an Order object, now uses the order number instead of it’s primary key.
0.9.2¶
- Minimum required version of django-filer is now 1.2.5.
- Minimum required version of djangocms-cascade is now 0.10.2.
- Minimum required version of djangoshop-stripe is now 0.2.0.
- Changed the default address models to be more generic. Please read the upgrade instructions if you are upgrading from 0.9.0 or 0.9.1.
- Fixed
shop.money.fields.decontruct()
to avoid repetitive useless generation of migration files. - Using cached_property decoration for methods
unit_price
andline_total
inshop.models.order.OrderItem
. - Fixed #333: Accessing the cart when there is no cart associated with a customer.
- Removed Apphook
shop.cms_apps.OrderApp
. This class now must be added to the project’scms_apps.py
. This allows the merchant to override theshop.rest.serializers.OrderListSerializer
andshop.rest.serializers.OrderDetailSerializer
. - Bugfix: declared django-rest-auth as requirement in setup.py.
- Refactored shop.models.deferred -> shop.deferred. This allows to add a check for pending mappings into the ready-method of the shop’s AppConfig.
- Prepared for Django-1.10: Replaced all occurrences of
django.conf.urls.patterns()
by a simple list. - Method
get_render_context
in classes extending fromdjango_filters.FilterSet
now must be aclassmethod
accepting a request object and the querystring. - Method
get_renderer_context
in classCMSPageProductListView
now fetches the rendering context for filtering after the queryset have been determined. This allows us to adopt the context. - Function
loadMore()
inCatalogListController
bypasses the existing search query. This allows to use hard coded links for tag search. - Using Python’s
Enum
class to declare customer states, such as UNRECOGNIZED, GUEST or REGISTERED. - Created a customized database field to hold the customers states, as stored by the above
Enum
. - Fixed: A server-side invalidated email addresses was accepted anyway, causing problems for returning customers.
- Renamed CMS Page IDs for better consistency:
*
personal-details
->shop-customer-details
to access the Customer Detail Page. *reset-password
->shop-password-reset
to access the Reset Password Page. * new:shop-register-customer
to access the Register User Page. - Moved all non-Python dependencies from
bower_components
intonode_modules
. - The breadcrumb now is responsible itself for being wrapped into a Bootstrap container.
- Use Sekizai processors from django-angular. Replaced
shop-ng-requires
againstng-requires
andshop-ng-config
againstng-config
.
0.9.1¶
- Support for Python 3
- Support for Django-1.9
- Added abstract classes class:shop.models.delivery.BaseDelivery and class:shop.models.delivery.BaseDeliveryItem for optional partial shipping.
0.9.0¶
- Separated class:shop.views.catalog.ProductListView into its base and the new class class:shop.views.catalog.CMSPageProductListView which already has added it appropriate filters.
- Moved
wsgi.py
into upper folder. - Prototype of
shop.cascade.DialogFormPluginBase.get_form_data
changed. It now acceptscontext
,instance
andplaceholder
. - Fixed: It was impossible to enter the credit card information for Stripe and then proceed to the next step. Using Stripe was possible only on the last step. This restriction has gone.
- It now also is possible to display a summary of your order before proceeding to the final purchasing step.
- To be more Pythonic, class:shop.models.cart.CartModelManager raises a
DoesNotExist
exception instead ofNone
for visiting customers. - Added method
filter_from_request
to class:shop.models.order.OrderManager. - Fixed: OrderAdmin doesn’t ignores error if customer URL can’t be resolved.
- Fixed: Version checking of Django.
- Fixed: Fieldsets duplication in Product Admin.
- CartPlugin now can be child of ProcessStepPlugin and BootstrapPanelPlugin.
- Added ShopAddToCartPlugin.
- All Checkout Forms now can be rendered as editable or summary.
- All Dialog Forms now can declare a legend.
- In
DialogFormPlugin
, methodform_factory
always returns a form class instead of an error dict if form was invalid. - Added method
OrderManager.filter_from_request
, which behaves analogous toCartManager.get_from_request
. - Fixed lookups using MoneyField by adding method get_prep_value.
- Dropped support for South migrations.
- Fixed: In
ProductIndex
, translations now are always overridden. - Added class
SyncCatalogView
which can be used to synchronize the cart with a catalog list view. - Content of Checkout Forms is handled by a single transaction.
- All models such as Product, Order, OrderItem, Cart, CartItem can be overridden by the merchant’s implementation. However, we are using the deferred pattern, instead of configuration settings.
- Categories must be implemented as separate django-SHOP addons. However for many implementations pages form the django-CMS can be used as catalog list views.
- The principle on how cart modifiers work, didn’t change. There more inversion of control now, in that sense, that now the modifiers decide themselves, how to change the subtotal and final total.
- Existing Payment Providers can be integrated without much hassle.
Since version 0.2.1 a lot of things have changed. Here is a short summary:¶
- The API of django-SHOP is accessible through a REST interface. This allows us to build MVC on top of that.
- Changed the two OneToOne relations from model Address to User, one was used for shipping, one for billing. Now abstract BaseAddress refers to the User by a single ForeignKey giving the ability to link more than one address to each user. Additionally each address has a priority field for shipping and invoices, so that the latest used address is offered to the client.
- Replaced model shop.models.User by the configuration directive
settings.AUTH_USER_MODEL
, to be compliant with Django documentation. - The cart now is always stored inside the database; there is no more distinction between session
based carts and database carts. Carts for anonymous users are retrieved using the visitor’s
session_key. Therefore we don’t need a utility function such
get_or_create_cart
anymore. Everything is handled by the a new CartManager, which retrieves or creates or cart based on the request session. - If the quantity of a cart item drops to zero, this items is not automatically removed from the cart. There are plenty of reasons, why it can make sense to have a quantity of zero.
- A WatchList (some say wish-list) has been added. This simply reuses the existing Cart model, where the item quantity is zero.
- Currency and CurrencyField are replaced by Money and MoneyField. These types not only store the
amount, but also in which currency this amount is. This has many advantages:
- An amount is rendered with its currency symbol as a string. This also applies for JSON data-structures, rendered by the REST framework.
- Money types of different currencies can not be added/substracted by accident. Normal installations woun’t be affected, since each shop system must specify its default currency.
- Backend pools for Payment and Shipping have been removed. Instead, a Cart Modifier can inherit
from
PaymentModifier
orShippingModifier
. This allows to reuse the Cart Modifier Pool for these backends and use the modifiers logic for adding extra rows to he carts total. - The models
OrderExtraRow
andOrderItemExtraRow
has been replaced by a JSONField extra_rows in modelOrderModel
andOrderItemModel
.OrderAnnotation
now also is stored inside this extra field. - Renamed for convention with other Django application:
- date_created -> created_at
- last_updated -> updated_at
- ExtraOrderPriceField -> BaseOrderExtraRow
- ExtraOrderItemPriceField -> BaseItemExtraRow
Version 0.2.1¶
This is the last release on the old code base. It has been tagged as 0.2.1 and can be examined for historical reasons. Bugs will not be fixed in this release.
Version 0.2.0¶
- models.FloatField are now automatically localized.
- Support for Django 1.2 and Django 1.3 dropped.
- Product model now has property
can_be_added_to_cart
which is checked before adding the product to cart - In cart_modifiers methods
get_extra_cart_price_field
andget_extra_cart_item_price_field
accepts the additional objectrequest
which can be used to calculate the price according to the state of a session, the IP-address or whatever might be useful. Note for backwards compatibility: Until version 0.1.2, instead of therequest
object, an empty Python dictionary namedstate
was passed into the cart modifiers. Thisstate
object could contain arbitrary data to exchange information between the cart modifiers. This Python dict now is a temporary attribute of therequest
object namedcart_modifier_state
. Use it instead of thestate
object. - Cart modifiers can add an optional
data
field besidelabel
andvalue
for both, the ExtraOrderPriceField and the ExtraOrderItemPriceField model. This extradata
field can contain anything serializable as JSON.
Version 0.1.2¶
cart_required and order_required decorators now accept a reversible url name instead and redirect to cart by default
Added setting SHOP_PRICE_FORMAT used in the priceformat filter
Separation of Concern in OrderManager.create_from_cart: It now is easier to extend the Order class with customized data.
Added OrderConfirmView after the shipping backend views that can be easily extended to display a confirmation page
Added example payment backend to the example shop
Added example OrderConfirmView to the example shop
Unconfirmed orders are now deleted from the database automatically
- Refactored order status (requires data migration)
- removed PAYMENT and added CONFIRMING status
- assignment of statuses is now linear
- moved cart.empty() to the PaymentAPI
- orders now store the pk of the originating cart
- Checkout process works like this:
CartDetails
- CheckoutSelectionView
- POST –> Order.objects.create_from_cart(cart) removes all orders originating from this cart that have status < CONFIRMED(30)
- creates a new Order with status PROCESSING(10)
- ShippingBackend
- self.finished() sets the status to CONFIRMING(20)
- OrderConfirmView
- self.confirm_order() sets the status to CONFIRMED(30)
- PaymentBackend
- self.confirm_payment() sets the status to COMPLETED(40)
- empties the related cart
- ThankYouView
- does nothing!
Version 0.1.1¶
- Changed CurrencyField default decimal precision back to 2
Version 0.1.0¶
- Bumped the CurrencyField precision limitation to 30 max_digits and 10 decimal places, like it should have been since the beginning.
- Made Backends internationalizable, as well as the BillingShippingForm thanks to the introduciton of a new optional backend_verbose_name attribute to backends.
- Added order_required decorator to fix bug #84, which should be used on all payment and shipping views
- Added cart_required decorator that checks for a cart on the checkout view #172
- Added get_product_reference method to Product (for extensibility)
- Cart object is not saved to database if it is empty (#147)
- Before adding items to cart you now have to use get_or_create_cart with save=True
- Changed spelling mistakes in methods from payed to paid on the Order model and on the API. This is potentially not backwards compatible in some border cases.
- Added a mixin class which helps to localize model fields of type DecimalField in Django admin view.
- Added this newly created mixin class to OrderAdmin, so that all price fields are handled with the correct localization.
- Order status is now directly modified in the shop API
- CartItem URLs were too greedy, they now match less.
- In case a user has two carts, one bound to the session and one to the user, the one from the session will be used (#169)
- Fixed circular import errors by moving base models to shop.models_bases and base managers to shop.models_bases.managers
Version 0.0.13¶
(Version cleanup)
Version 0.0.12¶
- Updated translations
- Split urls.py into several sub-files for better readability, and put in a urls shubfolder.
- Made templates extend a common base template
- Using a dynamically generated form for the cart now to validate user input. This will break your cart.html template. Please refer to the changes in cart.html shipped by the shop to see how you can update your own template. Basically you need to iterate over a formset now instead of cart_items.
- Fixed a circular import problem when user overrode their own models
Version 0.0.11¶
- Performance improvement (update CartItems are now cached to avoid unnecessary db queries)
- Various bugfixes
Version 0.0.10¶
- New hooks were added to cart modifiers: pre_process_cart and post_process_cart.
- [API change] Cart modifiers cart item methods now recieve a state object, that allows them to pass information between cart modifiers cheaply.
- The cart items are not automatically saved after process_cart_item anymore. This allows for cart modifiers that change the cart’s content (also deleting).
- Changed the version definition mechanism. You can now: import shop; shop.__version__. Also, it now conforms to PEP 386
- [API Change] Changed the payment backend API to let get_finished_url and get_cancel_url return strings instead of HttpResponse objects (this was confusing)
- Tests for the shop are now runnable from any project
- added URL to CartItemView.delete()
Version 0.0.9¶
- Changed the base class for Cart Modifiers. Methods are now expected to return a tuple, and not direectly append it to the extra_price_fields. Computation of the total is not done using an intermediate “current_total” attribute.
- Added a SHOP_FORCE_LOGIN setting that restricts the checkout process to loged-in users.
Version 0.0.8¶
- Major change in the way injecting models for extensibility works: the base models are now abstract, and the shop provides a set of default implementations that users can replace / override using the settings, as usual. A special mechanism is required to make the Foreign keys to shop models work. This is explained in shop.utils.loaders
Version 0.0.7¶
- Fixed bug in the extensibility section of CartItem
- Added complete German translations
- Added verbose names to the Address model in order to have shipping and billing forms that has multilingual labels.
Version 0.0.6¶
(Bugfix release)
- Various bugfixes
- Creating AddressModels for use with the checkout view (the default ones at least) were bugged, and would spawn new instances on form post, instead of updating the user’s already existing ones.
- Removed redundant payment method field on the Order model.
- The “thank you” view does not crash anymore when it’s refreshed. It now displays the last order the user placed.
- Fixed a bug in the shippingbilling view where the returned form was a from class instead of a from instance.
Version 0.0.5¶
- Fix a bug in 0.0.4 that made South migration fail with Django < 1.3
Version 0.0.4¶
- Addresses are now stored as one single text field on the Order objects
- OrderItems now have a ForeignKey relation to Products (to retrieve the product more easily)
- New templatetag (“products”)
- Made most models swappable using settings (see docs)
- Changed checkout views. The shop uses one single checkout view by default now.
- Created new mechanism to use custom Address models (see docs)
- Moved all Address-related models to shop.addressmodel sub-app
- Removed Client Class
- Removed Product.long_description and Product.short_description from the Product superclass
- Bugfixes, docs update
Version 0.0.3¶
- More packaging fixes (missing templates, basically)
Version 0.0.2¶
- Packaging fix (added MANIFEST.in)
Version 0.0.1¶
- Initial release to Pypi
Release notes for version 0.11¶
Since some payment providers require to have an existing order object before the payment is
fulfilled, method OrderManager.create_from_cart()
does not invoke the method
order.populate_from_cart()
anymore. This now must be performed by the payment service provider
in two steps:
order = OrderModel.objects.create_from_cart(cart, request)
order.populate_from_cart(cart, request)
Upgrading django-SHOP¶
Upgrading to 0.10.0¶
This version requires django-CMS version 3.4.2 or higher and djangocms-cascade version 0.12.0 or higher. It is well tested with Django-1.10 but should work as well with Django-1.9.
There has been a lot of effort in getting a cleaner and more consistent API. If you upgrade from version 0.9 please note the following changes:
The REST serializers have been moved into their own submodule shop.serializers
. They now are
separated into bases
and defaults
following the same naming convention as beeing used
in shop.models
and shop.admin
. Please ensure that you change your import statements.
Serializers ProductCommonSerializer
, ProductSummarySerializer
and ProductDetailSerializer
have been unified into a single ProductSerializer
, which acts as default for the
ProductListView
and the ProductRetrieveView
. The ProductSummarySerializer
(which is used
to serialize attributes available across all products of the site) now must be configured using the
settings directive SHOP_PRODUCT_SUMMARY_SERIALIZER
.
All Angular directives have been checked for HTML5 mode compatibility. It is strongly recommended over hashbang mode.
Billing and shipping address have been unified into one single address form which makes them easier
to interchange. The salutation
field has been removed from the address model and can now
optionally be added to the merchant representation.
All AngularJS directives for the catalog list and catalog search view support infinite scroll, as well as manual pagination.
After upgrading to angular-ui-bootstrap version 0.14, all corresponding directives have to be
prefixed with uib-...
.
There is no more need for a special URL pattern to handle auto-completion search. Instead use the
wrapping view shop.search.views.CMSPageCatalogWrapper
.
The model CartItem
has a new CharField product_code
. This replaces the product_code
,
which optionally is kept inside its extra
dict. This requires to simplify some templates
implementing {{ somevar.extra.product_code }}
into {{ somevar.product_code }}
; it applies to
the cart, the add-to-cart and the order templates. Also check for ProductSerializer
-s
implemented for products with variations.
Look for methods implementing get_product_variant
since its signature changed.
requires a database migration by the merchant implementation. Such a migration file must contain a datamigration, for instance:
from __future__ import unicode_literals
from django.db import migrations, models
def forwards(apps, schema_editor):
CartItem = apps.get_model('myshop', 'CartItem')
for item in CartItem.objects.all():
item.product_code = item.extra.get('product_code', '')
item.save()
def backwards(apps, schema_editor):
CartItem = apps.get_model('myshop', 'CartItem')
for item in CartItem.objects.all():
item.extra['product_code'] = item.product_code
item.save()
class Migration(migrations.Migration):
dependencies = [
('myshop', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='cartitem',
name='product_code',
field=models.CharField(blank=True, help_text='Product code of added item.', max_length=255, null=True, verbose_name='Product code'),
),
migrations.RunPython(forwards, reverse_code=backwards),
]
0.9.3¶
This version requires djangocms-cascade 0.11.0 or higher. Please ensure to run the migrations which convert the Cascade elements:
./manage.py migrate shop
0.9.2¶
The default address models have changed in 0.9.2. If you are upgrading from 0.9.0 or 0.9.1 and your project is using the default address models, you need to add a migration to make the necessary changes to your models:
./manage.py makemigrations --empty yourapp
Next, edit the migration file to look like this:
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
# makemgirations will generate the dependencies for you.
]
operations = [
migrations.RenameField("ShippingAddress", "addressee", "name"),
migrations.RenameField("ShippingAddress", "street", "address1"),
migrations.RenameField("ShippingAddress", "supplement", "address2"),
migrations.RenameField("ShippingAddress", "location", "city"),
migrations.AlterField("ShippingAddress", "name", models.CharField(
verbose_name="Full name", max_length=1024
)),
migrations.AlterField("ShippingAddress", "address1", models.CharField(
verbose_name="Address line 1", max_length=1024
)),
migrations.AlterField("ShippingAddress", "address2", models.CharField(
verbose_name="Address line 2", max_length=1024
)),
migrations.AlterField("ShippingAddress", "city", models.CharField(
verbose_name="City", max_length=1024
)),
migrations.RenameField("BillingAddress", "addressee", "name"),
migrations.RenameField("BillingAddress", "street", "address1"),
migrations.RenameField("BillingAddress", "supplement", "address2"),
migrations.RenameField("BillingAddress", "location", "city"),
migrations.AlterField("BillingAddress", "name", models.CharField(
verbose_name="Full name", max_length=1024
)),
migrations.AlterField("BillingAddress", "address1", models.CharField(
verbose_name="Address line 1", max_length=1024
)),
migrations.AlterField("BillingAddress", "address2", models.CharField(
verbose_name="Address line 2", max_length=1024
)),
migrations.AlterField("BillingAddress", "city", models.CharField(
verbose_name="City", max_length=1024
)),
]
Finally, apply the migration:
./manage.py migrate yourapp
Contributing¶
Naming conventions¶
The official name of this project is django-SHOP. Third party plugins for django-SHOP shall
follow the same naming convention as for plugins of django-CMS: Third party package names shall
start with djangoshop followed by a dash; no space or dash shall be added between django and
shop, for example: djangoshop-stripe
Django-SHOP should be capitalised at the start of sentences and in title-case headings.
Running tests¶
It’s important to run tests before committing :)
Setting up the environment¶
We highly suggest you run the tests suite in a clean environment, using a tool such as virtualenv.
- Clone the repository and cd into it:
git clone https://github.com/awesto/django-shop
cd django-shop
- Create a virtualenv, and activate it:
virtualenv ~/.virtualenvs/django-shop
source ~/.virtualenvs/django-shop/bin/activate
- Install the project in development mode:
pip install -e .
- Install the development requirements:
pip install -r requirements/test_py3.txt
That’s it! Now, you should be able to run the tests:
export DJANGO_SHOP_TUTORIAL=polymorphic
py.test example
We use tox as a CI tool. To run the full CI test suite and get a coverage report, all you have to do is this:
pip install tox
tox
If you work on a certain part of the code base and you want to run the related tests, you may only
want to run the tests affecting that part. In such a case use py.test
from your testing
environment and specify the file to test, or for more granularity the class name or even the method
name. Here are two examples:
py.test testshop/test_money.py
py.test testshop/test_money.py -k test_pickle
Measuring which lines of code have been “seen” be the test runner is an important task while testing. Do this by creating a coverage report, for example with:
coverage run $(which py.test) testshop
coverage report
or if you to test only a specific class
coverage run .tox/py27-django19/bin/py.test testshop/test_money.py
coverage report -m shop/money/*.py
Note
Using tox and py.test is optional. If you prefer the conventional way of running tests, you can
do this: django-admin.py test tests --settings shop.testsettings
.
Community¶
Most of the discussion around django SHOP takes place on IRC (Internet Relay Chat), on the freenode servers in the #django-shop channel.
We also have a mailing list and a google group:
http://groups.google.com/group/django-shop
Code guidelines¶
Unless otherwise specified, follow PEP 8 as closely as possible.
An exception to PEP 8 is our rules on line lengths. Don’t limit lines of code to 79 characters if it means the code looks significantly uglier or is harder to read. Consider 100 characters as a soft, and 119 as a hard limit. Here soft limit means, that unless a line must be splitted across two lines, it is more readable to stay with a long line.
Use the issue tracker only to report bugs. Send unsolicited pull requests only to fix bug – never to add new features.
Use stack-overflow to ask for questions related to django-SHOP.
Most pull requests will be rejected without proper unit testing.
Before adding a new feature, please write a specification using the style for Django Enhancement Proposals.
More information about how to send a Pull Request can be found on GitHub: http://help.github.com/send-pull-requests/
CORE DEVELOPERS¶
- Jacob Rief
- René Fleschenberg
CONTRIBUTORS¶
- abelradac
- Adrien Lemaire
- airtonix
- Aleš Kocjančič
- Anders Petersson
- Andrés Reyes Monge
- Arturo Fernandez
- Audrey Roy
- Benjamin Wohlwend
- Ben Lopatin
- Bojan Mihelac
- Chris Morgan
- Dino Perovic
- fivethreeo
- German Larrain
- Hamza Khchine
- ikresoft
- Issac Kelly
- Jacek Mitręga
- Jonas Obrist
- Justin Steward
- Kristian Øllegaard
- maltitco
- Maltsev Artyom
- Martin Ogden
- Mike Yumatov
- Mikhail Kolesnik
- Pavel Zhukov
- Pedro Gracia
- Per Rosengren
- Raúl Cumplido
- Roberth Solís
- Rolo Mawlabaux
- rubengrill
- Simon Luijk
- Sławomir Ehlert
- Stephen Muss
- Thomas Woolford
RETIRED CORE DEVELOPERS¶
- Chris Glass (chrisglass)
- Martin Brochhaus
To be written¶
Address Model¶
DjangoSHOP is shipped with a default address model as found in
shop.models.defaults.address.ShippingAddress
and
shop.models.defaults.address.BillingAddress
. It is based on a recommendation on
International Address Fields in Web Forms.
Some people might feel that this address model is not suitable for their specific use-case, or in their selling region. Since django-SHOP allows to override each model, we simply might want to write our own one.
Overriding the Default Models¶
To start with, have a look at the implementation of the default address models mentioned before. Chances are high that you might want to use the same address model for shipping, as well as for billing. Therefore instead of writing them twice, we use a mixin class:
from django.db import models
class AddressModelMixin(models.Model):
name = models.CharField("Full name", max_length=1024)
address1 = models.CharField("Address line 1", max_length=1024)
address2 = models.CharField("Address line 2", max_length=1024, blank=True, null=True)
zip_code = models.CharField("ZIP", max_length=12)
city = models.CharField("City", max_length=1024)
class Meta:
abstract = True
This mixin class then is used to instantiate the billing address models:
from shop.models.address import BaseShippingAddress, BaseBillingAddress
class BillingAddress(BaseBillingAddress, AddressModelMixin):
class Meta:
verbose_name = "Billing Address"
In Europe for B2B commerce, the customer’s tax number is associated with the location for delivery. We therefore have to add it to our shipping address models:
- class ShippingAddress(BaseShippingAddress, AddressModelMixin):
tax_number = models.CharField(“Tax number”, max_length=100)
- class Meta:
- verbose_name = “Shipping Address”
Multiple Addresses¶
Depending on the shop’s requirements, each customer may have no, one or multiple billing- and/or shipping addresses. On an e-commerce site selling digital goods, presumably only the billing address makes sense. A shop with many returning customers probably wants to allow them to store more than one address.
In django-SHOP each address model has a foreign key onto the customer model, hence all of the above use-cases are possible.
Rendering the Address Forms¶
During checkout
How to create a Payment backend¶
Payment backends must be listed in settings.SHOP_PAYMENT_BACKENDS
Shop interface¶
While we could solve this by defining a superclass for all payment backends, the better approach to plugins is to implement inversion-of-control, and let the backends hold a reference to the shop instead.
The reference interface for payment backends is located at
-
class
shop.payment.api.
PaymentAPI
¶
Currently, the shop interface defines the following methods:
Common with shipping¶
-
PaymentAPI.
get_order
(request)¶ Returns the order currently being processed.
Parameters: request – a Django request object Return type: an Order
instance
-
PaymentAPI.
add_extra_info
(order, text)¶ Adds an extra info field to the order (whatever)
Parameters: - order – an
Order
instance - text – a string containing the extra order information
- order – an
-
PaymentAPI.
is_order_payed
(order)¶ Whether the passed order is fully paid or not
Parameters: order – an Order
instanceReturn type: bool
-
PaymentAPI.
is_order_complete
(order)¶ Whether the passed order is in a “finished” state
Parameters: order – an Order
instanceReturn type: bool
-
PaymentAPI.
get_order_total
(order)¶ Returns the order’s grand total.
Parameters: order – an Order
instanceReturn type: Decimal
-
PaymentAPI.
get_order_subtotal
(order)¶ Returns the order’s sum of item prices (without taxes or S&H)
Parameters: order – an Order
instanceReturn type: Decimal
-
PaymentAPI.
get_order_short_name
(order)¶ A short human-readable description of the order
Parameters: order – an Order
instanceReturn type: a string with the short name of the order
-
PaymentAPI.
get_order_unique_id
(order)¶ The order’s unique identifier for this shop system
Parameters: order – an Order
instanceReturn type: the primary key of the Order
(in the default implementation)
-
PaymentAPI.
get_order_for_id
(id)¶ Returns an
Order
object given a unique identifier (this is the reverse ofget_order_unique_id()
)Parameters: id – identifier for the order Return type: the Order
object identified byid
Specific to payment¶
-
PaymentAPI.
confirm_payment
(order, amount, transaction_id, save=True)¶ This should be called when the confirmation from the payment processor was called and that the payment was confirmed for a given amount. The processor’s transaction identifier should be passed too, along with an instruction to save the object or not. For instance, if you expect many small confirmations you might want to save all of them at the end in one go (?). Finally the payment method keeps track of what backend was used for this specific payment.
Parameters: - order – an
Order
instance - amount – the paid amount
- transaction_id – the backend-specific transaction identifier
- save – a
bool
that indicates if the changes should be committed to the database.
- order – an
Backend interface¶
The payment backend should define the following interface for the shop to be able do to anything sensible with it:
Attributes¶
-
PaymentBackend.
backend_name
¶ The name of the backend (to be displayed to users)
-
PaymentBackend.
url_namespace
¶ “slug” to prepend to this backend’s URLs (acting as a namespace)
Methods¶
-
PaymentBackend.
__init__
(shop)¶ must accept a “shop” argument (to let the shop system inject a reference to it)
Parameters: shop – an instance of the shop
-
PaymentBackend.
get_urls
()¶ should return a list of URLs (similar to urlpatterns), to be added to the URL resolver when urls are loaded. These will be namespaced with the url_namespace attribute by the shop system, so it shouldn’t be done manually.
Security¶
In order to make your payment backend compatible with the SHOP_FORCE_LOGIN
setting please make sure to add the @shop_login_required
decorator to any
views that your backend provides.
Multi-Tenancy¶
If a site built with the django-SHOP framework shall be used by more than one vendor, we speak about a multi-tenant environment. Django-SHOP does not implement multi-tenancy out of the box, it however is quite simple to extend and customize.
Terminology¶
To distinguish the roles in a multi-tenant environment, we define the merchant as the site owner. On the other side, a vendor owns a range of products. Each new product, he adds to the site, is assigned to him. Later on, existing products can only be modified and deleted by the vendor they belong to.
Product Model¶
Since we are free to declare our own product models, This can be achieved by adding a foreign key onto the User model:
from shop.models.product import BaseProduct
class Product(BaseProduct):
# other product attributes
merchant = models.ForeignKey(
User,
verbose_name=_("Merchant"),
limit_choices_to={'is_staff': True},
)
Note
unfinished docs
How to secure your catalog views¶
Chances are that you don’t want to allow your users to browse all views of the shop as anonymous users.
License¶
Django-SHOP is licensed under the terms of the BSD license.