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
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.
1.1. 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.
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
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
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.
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',
...
)
1.2.2. 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',
...
)
1.2.3. 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.
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
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.
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 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.
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
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.
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 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.
1.4. 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 . |
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.
1.4.1.1. 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.