Installation instructions

Installation

This document describes the steps needed to get Plata up and running.

Plata is based on Django, so you need a working Django installation first. Plata is developed using Django 1.10, and is not tested against any earlier version.

You can download a version of plata using pip:

$ pip install -e git+https://github.com/fiee/plata@next#egg=Plata

Please note that the package installable with pip only contains the files needed to run plata. It does not include documentation, tests or the example project which comes with the development version, which you can download using the Git version control system:

$ git clone git://github.com/fiee/plata.git -b next

Plata requires a version of simplejson >=2.1 because older versions cannot serialize decimal values, only floats.

In addition, you will need PDFDocument if you want to generate PDFs.

Configuration

Writing your product model

First, you need to write your own product and price model. There is a small (and documented) interface contract described in Contracts.

The smallest possible implementation while still following best practice follows here:

from django.db import models
from django.utils.translation import ugettext_lazy as _

from plata.product.models import ProductBase
from plata.shop.models import PriceBase

class Product(ProductBase):
    name = models.CharField(_('name'), max_length=100)
    slug = models.SlugField(_('slug'), unique=True)
    description = models.TextField(_('description'), blank=True)

    class Meta:
        ordering = ['name']
        verbose_name = _('product')
        verbose_name_plural = _('products')

    def __unicode__(self):
        return self.name

    @models.permalink
    def get_absolute_url(self):
        return ('plata_product_detail', (), {'slug': self.slug})

class ProductPrice(PriceBase):
    product = models.ForeignKey(Product, verbose_name=_('product'),
        related_name='prices')

    class Meta:
        get_latest_by = 'id'
        ordering = ['-id']
        verbose_name = _('price')
        verbose_name_plural = _('prices')

Plata has to know the location of your shop model, because it is referenced e.g. in the product ForeignKey of order items. If the product model exists in the myapp Django application, add the following setting:

PLATA_SHOP_PRODUCT = 'myapp.Product'

Adding the modules to INSTALLED_APPS

INSTALLED_APPS = (
    ...
    'myapp',
    'plata',
    'plata.contact', # Not strictly required (contact model can be exchanged)
    'plata.discount',
    'plata.payment',
    'plata.shop',
    ...
    )

You should run ./manage.py makemigrations plata and ./manage.py migrate plata after you’ve added the required modules to INSTALLED_APPS.

Creating the Shop object

Most of the shop logic is contained inside Shop. This class implements cart handling, the checkout process and handing control to the payment modules when the order has been confirmed. There should exist exactly one Shop instance in your site (for now).

The Shop class requires three models and makes certain assumptions about them. The aim is to reduce the set of assumptions made or at least make them either configurable or overridable.

The models which need to be passed when instantiating the Shop object are

  • Contact
  • Order
  • Discount

Example:

from plata.contact.models import Contact
from plata.discount.models import Discount
from plata.shop.models import Order
from plata.shop.views import Shop

shop = Shop(
    contact_model=Contact,
    order_model=Order,
    discount_model=Discount,
    )

The shop objects registers itself in a central place, and can be fetched from anywhere using:

import plata
shop = plata.shop_instance()

The Shop class instantiation may be in myapp.urls or myapp.views or somewhere similar, it’s recommended to put the statement into the views.py file because the Shop class mainly offers views (besides a few helper functions.)

Adding views and configuring URLs

The Shop class itself does not define any product views. You have to do this yourself. You may use Django’s generic views or anything else fitting your needs.

Generic views using plata.shop_instance() could look like this:

from django import forms
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import ugettext_lazy as _
from django.views import generic

import plata
from plata.contact.models import Contact
from plata.discount.models import Discount
from plata.shop.models import Order
from plata.shop.views import Shop

from myapp.models import Product

shop = Shop(
    contact_model=Contact,
    order_model=Order,
    discount_model=Discount,
    )

product_list = generic.ListView.as_view(
    queryset=Product.objects.all(),
    paginate_by=10,
    template_name='product/product_list.html',
    )


class OrderItemForm(forms.Form):
    quantity = forms.IntegerField(label=_('quantity'), initial=1,
        min_value=1, max_value=100)


def product_detail(request, slug):
    product = get_object_or_404(Product, slug=slug)

    if request.method == 'POST':
        form = OrderItemForm(request.POST)

        if form.is_valid():
            # Referencing the shop object instantiated above
            order = shop.order_from_request(request, create=True)
            try:
                order.modify_item(product, relative=form.cleaned_data.get('quantity'))
                messages.success(request, _('The cart has been updated.'))
            except forms.ValidationError, e:
                if e.code == 'order_sealed':
                    [messages.error(request, msg) for msg in e.messages]
                else:
                    raise

            return redirect('plata_shop_cart')
    else:
        form = OrderItemForm()

    return render(request, 'product/product_detail.html', {
        'object': product,
        'form': form,
        })

Next, you need to add the Shop’s URLs to your URLconf:

from django.conf.urls import include, url
from myapp.views import shop, product_list, product_detail

urlpatterns = [
    url(r'^shop/', include(shop.urls)),
    url(r'^products/$',
        product_list,
        name='plata_product_list'),
    url(r'^products/(?P<slug>[-\w]+)/$',
        product_detail,
        name='plata_product_detail'),
]

The context processor

You can add plata.context_processors.plata_context to your settings.TEMPLATE_CONTEXT_PROCESSORS. This will add the following variables to your template context if they are available:

  • plata.shop: The current plata.shop.views.Shop instance
  • plata.order: The current order
  • plata.contact: The current contact instance
  • plata.price_includes_tax: Whether prices include tax or not

Alternatively you can also just overwrite the get_context method in your shop class.

Setting up logging

Plata uses Python’s logging module for payment processing, warnings and otherwise potentially interesting status changes. The logging module is very versatile and sometimes difficult to configure, because of this an example configuration is provided here. Put the following lines into your settings.py, adjusting the logfile path:

import logging, os
import logging.handlers

LOG_FILENAME = os.path.join(APP_BASEDIR, 'log', 'plata.log')

plata_logger = logging.getLogger('plata')
plata_logger.setLevel(logging.DEBUG)
plata_logging_handler = logging.handlers.RotatingFileHandler(LOG_FILENAME,
    maxBytes=10*1024*1024, backupCount=15)
plata_logging_formatter = logging.Formatter('%(asctime)s %(levelname)s:%(name)s:%(message)s')
plata_logging_handler.setFormatter(plata_logging_formatter)
plata_logger.addHandler(plata_logging_handler)

Implementing the shop as FeinCMS application content

To use the shop as application content you have to overwrite the render and redirect methods on your shop class. Take a look at this example in myshop.views:

from feincms.content.application.models import app_reverse
from plata.shop.views import Shop

class CustomShop(Shop):

    def render(self, request, template, context):
        """ render for application content """
        return template, context

    def redirect(self, url_name):
        return redirect(app_reverse(url_name, 'myshop.urls'))

    base_template = 'site_base.html'

shop = CustomShop(Contact, Order, Discount)