1. 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
is_active, we can fool an application to interpret such a guest visitor as a faked
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
1.1. Properties of the Customer Model¶
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
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
It also must define all the fields which are available in the default model as found in
By setting the flag
is_active = False, we can create guests inside Django’s
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.
1.2. 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
from shop.models.defaults.customer import Customer
or, if we need extra fields, then instead of the above, we create a customized
from shop.models.customer import BaseCustomer class Customer(BaseCustomer): birth_date = models.DateField("Date of Birth") # other customer related fields
1.2.1. 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.
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
MIDDLEWARE_CLASSES = ( ... 'django.contrib.auth.middleware.AuthenticationMiddleware', 'shop.middleware.CustomerMiddleware', ... )
1.2.2. Configure the Context Processors¶
Additionally, some templates may need to access the customer object through the
Therefore, add this context processor to the
settings.py of the project.
TEMPLATE_CONTEXT_PROCESSORS = ( ... 'shop.context_processors.customer', ... )
1.2.3. Implementation Details¶
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
customer.user.password. Some common fields and methods from the
accessible directly, when working with a
Customer object. Saving an instance of type
Customer also invokes the
save() method from the associated
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
>>> 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.
1.2.4. 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
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.
1.2.5. 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.
1.2.6. 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.
1.3. 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
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 is almost identical to the existing
User model as found in
django.contrib.auth.models.py. The difference is that it uses the field
username for looking up the credentials. To activate this alternative User model, add that
alternative authentication app to the project’s
INSTALLED_APPS = ( 'django.contrib.auth', 'email_auth', ... ) AUTH_USER_MODEL = 'email_auth.User'
User model uses the same database table as the Django authentication
auth_user. It is even field-compatible with the built-in model and hence
can be added later to an existing Django project.
1.3.1. 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
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
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.
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.
1.3.2. 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
from django.contrib import admin from shop.admin.customer import CustomerProxy, CustomerAdmin admin.site.register(CustomerProxy, CustomerAdmin)
This administration backend recycles the built-in
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.
1.4. Summary for Customer to User mapping¶
This table summarizes to possible mappings between a Django
User model  and the Shop’s
|Shop’s Customer Model||Django’s User Model||Active Session|
||Yes, but not logged in|
||Yes, but not logged in|
||Yes, but not logged in|
||Yes, logged in using Django’s authentication backend|
|||or any alternative |
|||if setting |
|||if setting |
1.4.1. 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.
188.8.131.52. 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.