The customer profile

We will now build a very lightweight profile model to store additional information about our customers. The Customer model will initially include three fields: a ForeignKey to the User model, an address, and a phone number. Because the profile model is simply a regular Django model class, any additional information can be added using the usual fields.

To simplify this model design slightly, we will also be creating a CustomerAddress model that represents a specific address. It will include fields such as three lines of address information (apartment number, street name, and so on), city, postal code, and state information. The CustomerAddress model will be referenced as a ForeignKey from the Customer profile.

Here is the sample code for the profile model:

class Customer(models.Model):
'''
The Customer model represents a customer of the online store. It extends Django's built-in auth.User model, which contains information such as first and last name, and e-mail, and adds phone number and address information.
'''
    user = models.ForeignKey(User)
    address = models.ForeignKey('CustomerAddress')
    phone_number = PhoneNumberField(blank=True)

classCustomerAddress(models.Model):
    '''
    The CustomerAddress model represents a customer's address. It is biased in favor of US addresses, but should contain enough fields to also represent addresses in other countries.
    '''
    line_1 = models.CharField(max_length=300)
    line_2 = models.CharField(max_length=300)
    line_3 = models.CharField(max_length=300)
    city = models.CharField(max_length=150)
    postalcode = models.CharField(max_length=10)
    state = USStateField(blank=True)
    country = models.CharField(max_length=150)

The idea here is that our USStateField is optional, with postal code and country information captured for non-US addresses. This could be modified to be even more flexible for international addresses by using a CharField instead of USStateField.

These models will live in an application called customers in the coleman framework. Once we've created them, we will need to do two things to activate the Customer class as an extension to the User model. First, add 'coleman.customers' to INSTALLED_APPS, then, set AUTH_PROFILE_MODULE='customers.Customer'. These two steps will also allow django-profiles, if in use, to automatically detect the profile model.

Taking orders: Models

Now that we have a way for customers to create accounts and update their profiles with their address information, we can build a simple order taking system. Broadly speaking, there are three parts to the order taking process:

  • A set of views to allow customers to manage their orders. This includes a shopping cart utility that they can view at any time. It also includes an order review/check-out page.
  • The checkout process itself. This is where the order is translated from our internal representation to a format that can be processed by a payment processor. For example, for Google Checkout's API, an XML document is created and submitted via HTTP Post to a Checkout endpoint on Google's servers.
  • An order model to store data about an order, what products are involved and who is doing the ordering. This will usually only be stored after an order is completed, so that customers and site staff can review or confirm the details.

We will begin by constructing a model to represent orders. Even though in the ordering process this is the last task we perform, we need to know exactly what information to gather during the earlier parts of the process. The best way to do this is to define the model in advance and build views, a shopping cart, and checkout processor around this data.

A simple order model has two main actors: the customer doing the ordering and the products they want. Additional data we will probably need include things such as:

  • The date the order was placed
  • The total price of the order
  • The price of individual products in the order and a status of where the order currently stands

This model looks something like the following:

class Order(models.Model):
    '''
    The Order model represents a customer order. It includes a ManyToManyField of products the customer is ordering and stores the date and total price information.
    '''
    customer = models.ForeignKey(User, blank=True, null=True)
    status_code = models.ForeignKey('StatusCode')
    date_placed = models.DateTimeField()
    total_price = models.DecimalField(max_digits=7, decimal_places=2)
    comments = models.TextField(blank=True)
    products = models.ManyToManyField(Product, through='ProductInOrder')

Notice that we have built the order model around Django's built-in User object instead of our Customer profile model. This is partly because it's easier to work with User objects in views, but also to prevent changes to the profile model from affecting our orders. The User model is mostly guaranteed to never change, whereas if we decided to replace our Customer profile model, deleted a profile accidentally, or had some other change, it could damage or delete our Order information.

There are two relationship fields in the Order model, status_code and products. We will discuss status codes first. This field is intended to function as a tracking system. Using a ForeignKey and a StatusCode model lets us add new statuses to reflect future changes in our ordering process, but still require all Orders to have a standard set of statuses for filtering and other purposes. The StatusCode model looks like this:

classStatusCode(models.Model):
    '''
    The StatusCode model represents the status of an order in the system.
    '''
    short_name = models.CharField(max_length=10)
    name = models.CharField(max_length=300)
    description = models.TextField()

Returning to our fictitious business example from Chapter 2, Setting Up Shop in 30 Minutes, imagine that CranStore.com has a four-step ordering process. This includes orders that are newly placed, those that have been shipped, those that were shipped and received by the customer, and those that have a problem. These status conditions map to the StatusCode model using the following values for short_name: NEW, SHIPPED, CLOSED, EXCEPTION. Additional details as to what each state involves can be added to the name and description fields for use on customer-friendly status tracking pages.

The second relationship field in the Order model is significantly more complicated and probably the most important field for all orders. This is the ManyToManyField called products. This field relates each order to a set of Product models from our design in Chapter 2. However, this relationship from Orders to Products requires a lot of additional information that pertains to the actual relationship, not the order or product themselves.

For example, CranStore.com sells a lot of canned Cranberries around late November. When a customer places an order for canned Cranberries, we need to know not just what they ordered, but how many cans they wanted and how much we charged them for each can. A simple ManyToManyField is not sophisticated enough to capture this information. This is the purpose behind this field's through argument.

In Django the through argument on a ManyToManyField lets us store information about the relationship between two models in an intermediary model. This way we can save things like of a product in an order, without too much complexity in our design. The downside of using the through argument is that we can no longer perform operations such as add and create, directly on an object instance's ManyToManyField.

If our Order model used a standard ManyToManyField, we could add Product objects to an order simply by calling add like so:

order_object.products.add(cranberry_product)

This is no longer possible with the through field, we must instead create an instance of the intermediary model directly to add something to our Order object's products field, as in the following:

ProductInOrder.objects.create(order=order_object, product=cranberry_product)

This creates a new ProductInOrder instance and updates the products field of order_object to include our cranberry_product. We can still use the usual ManyToManyFieldQuerySet operations, such as filter and order_by, on fields using the through argument.

In addition we can filter the Order model using fields on the ProductInOrder intermediary model. To find all open Orders that include canned Cranberries, we could perform the following:

Product.objects.filter(productinorder__product=cranberry_product,
status_code__short_name='OPEN')

The full ProductInOrder model is listed below:

classProductInOrder(models.Model):
    '''
    The ProductInOrder model represents information about a specific product ordered by a customer.
    '''
    order = models.ForeignKey(Order)
    product = models.ForeignKey(Product)
    unit_price = models.DecimalField(max_digits=7, decimal_places=2) 
    total_price = models.DecimalField(max_digits=7, decimal_places=2)
    quantity = models.PositiveIntegerField()
    comments = models.TextField(blank=True)

These three models represent our basic order taking system. Now that we have an idea of the data we will be collecting for each order, we can begin building a set of views so that customers can place orders through the Web.

Taking orders: Views

We will build four views for our initial order taking system. These will perform basic e-commerce operations such as: review a shopping cart, add an item to the cart, remove an item from the cart, and checkout. We will discuss the mechanics of the shopping cart system in the next section. For now, we will just focus on these four operations, how they translate to Django views, and what their templates would look like.

The most important view is the shopping_cart view, which allows a customer to review the products they have selected for purchase. The code for this view appears below:

defshopping_cart(request, template_name='orders/shopping_cart.html'):
    '''
    This view allows a customer to see what products are currently in their shopping cart.
    '''
    cart = get_shopping_cart(request)
    ctx = {'cart': cart}
    return render_to_response(template_name, ctx,
	
context_instance=RequestContext(request))

The view retrieves the shopping cart object for this request, adds it to the template context, and returns a rendered template whose only variable is the shopping cart, called cart. We will discuss get_shopping_cart shortly, but for now let's just note that we are using this as a helper function in our views. The retrieval operation is very simple and would easily fit on one line in this view function. However, by using a helper function, if we ever need to make changes in the future to how our cart works, we have a single piece of code to change and can avoid having to touch every view.

The next two operations are related: add_to_cart and remove_from_cart. The code for these views is as follows:

def add_to_cart(request, queryset, object_id=None, slug=None,
                slug_field='slug', template_name='orders/add_to_cart.html'):
    '''
    This view allows a customer to add a product to their shopping cart. A single GET parameter can be included to specify the quantity of the product to add.
    '''
    obj = lookup_object(queryset, object_id, slug, slug_field)
    quantity = request.GET.get('quantity', 1)
    cart = get_shopping_cart(request)
    cart.add_item(obj, quantity)
    update_shopping_cart(request, cart)
    ctx = {'object': obj, 'cart': cart}
    return render_to_response(template_name, ctx,
context_instance=RequestContext(request))

def remove_from_cart(request, cart_item_id,
                     template_name='orders/remove_from_cart.html'):
    '''
    This view allows a customer to remove a product from their shopping cart. It simply removes the entire product from the cart, without regard to quantities.
    '''
    cart = get_shopping_cart(request)
    cart.remove_item(cart_item_id)
    update_shopping_cart(request, cart)
    ctx = {'cart': cart}
    return render_to_response(template_name, ctx,
                                             context_instance=RequestContext(request))

A careful review of these two functions will reveal that they have two different interfaces for managing cart items. The add_to_cart view works with an object directly, an instance of our Product model from Chapter 2, usually. The remove_from_cart function needs a cart_item_id variable. This is simply an integer value that indexes into the shopping cart's item list.

We must use two different interfaces here, because once we've added a product to the cart, we need a unique way of talking about that particular product at that particular index in the cart. If our remove function tries to remove items based on the item's model or a primary key value, we would have difficulty when the same item has been added to our cart twice.

The solution is to give every item added to the cart a unique identifier. The Cart model in the next section handles this unique ID internally; all we need to do is render it in our templates when necessary. This way we can add arbitrary objects to our shopping cart as often as we want and retain the ability to remove a specific addition, whether it was performed in error or because the customer changed their mind.

When adding an item to the cart, we simply need to look up the item's slug or object_id value from the Django ORM and store the object directly into the cart. When added, the cart will assign it an identifier.

There is another helper function in these views called lookup_object, which performs the actual retrieval of our Product or other object that we're adding to the cart. This helper function looks like this:

def lookup_object(queryset, object_id=None, slug=None, slug_field=None):
    if object_id is not None:
        obj = queryset.get(pk=object_id)
    elif slug and slug_field:
        kwargs = {slug_field: slug}
        obj = queryset.get(**kwargs)
     else:
         raise Http404
     return obj

This allows some flexibility and reuse for our add_to_cart view, because we can define which lookup fields (object_id or slug and slug_field) to use for retrieving the object. If we decided to use a different Product model than the one written in Chapter 2 and this alternative model did not use slug fields as identifiers, then we would still be able to use our add_to_cart view without any modification. Separating the lookup with a helper function allows us to write clean, short, and easy to read views.

This view is not so flexible, however, that we can feel comfortable calling it a "generic view", without some additional work. Generic views, patterned after Django's built-in generic views discussed in Chapter 2, allow almost every aspect of their functionality to be configurable. For example, if these views were really generic, we could specify a custom class to use as our shopping cart, instead of what get_shopping_cart gives us. We could even add a keyword argument to get_shopping_cart that would allow different cart classes and pass through a value from our view's arguments.

The last view in our order taking system is the checkout view. This will act as a stepping stone into our payment processor system. In the previous chapter we saw a very simple payment processor; we will build a more advanced one here, and discuss the issue in detail in Chapter 4. The checkout view is listed below, note that it is specifically designed for the Google Checkout API we discussed previously:

def checkout(request, template_name='orders/checkout.html'):
     '''
     This view presents the user with an order confirmation page and the final order button to process their order through a checkout system.
     '''
     cart = get_shopping_cart(request)
     googleCart, googleSig = sign_google_cart(cart)
     ctx = {'cart': cart,
               'googleCart': googleCart,
               'googleSig': googleSig,
               'googleMerchantKey': settings.GOOGLE_MERCHANT_KEY,
               'googleMerchantID': settings.GOOGLE_MERCHANT_ID}
     return render_to_response(template_name, ctx,
                                              context_instance=RequestContext(request))

The various Google context variables are needed to compose a Checkout API form in the rendered template. The additional merchant key and ID information is stored in our project's settings file. There is also a helper function, sign_google_cart, which generates a Google-compatible representation of our shopping cart as well as a cryptographic signature.

The Google-compatible representation of our shopping cart is simply an XML file that conforms to the specifications documented by the Checkout API. The signature is a base64 encoded HMAC SHA-1 signature generated from the shopping cart XML and our secret Google merchant key. This prevents tampering when our checkout form is submitted to the Google Checkout system. More information on this process is available at: http://code.google.com/apis/checkout/developer/index.html.

We will write these functions and examine the XML file in detail later in this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset