Architecture

This document should gather all the pre-code architecture requirements/research.

Core system

Generally, the shop system can be seen as two different phases, with two different problems to solve:

The shopping phase:

From a user perspective, this is where you shop around different product categories, and add desired products to a shopping cart (or other abstraction). This is a very well-know type of website problematic from a user interface perspective as well as from a model perspective: a simple “invoice” pattern for the cart is enough.

The complexity here is to start defining what a shop item should be.

The checkout process:

As the name implies, this is a “workflow” type of problem: we must be able to add or remove steps to the checkout process depending on the presence or absence of some plugins. For instance, a credit-card payment plugin whould be able to insert a payment details page with credit card details in the general workflow.

To solve this we could implement a workflow engine. The person implementing the webshop whould then define the process using the blocks we provide, and the system should then “run on its own”.

Random ideas:

  • multiple shops (site and prefixed)

    • namespaced urls for shops

      psuedocode , is this possible and or a good idea? requires restarts like the cms apphooks.

      prefix = shop.prefix
      # shopsite.get_urls(shop) # returns a tuple of (urlpatterns, app_name, shop_namespace)
      url(prefix, shopsite.get_urls(shop), kwargs={'shop_prefix': prefix})
      

      on a product

      def get_product_url(self):
         return reverse('shop_%s:product_detail' % threadlocals.shop_pk, kwargs={'category_slug': self.category_slug, slug=product.slug})
    • middleware to find current shop based on site and or prefix/ set current shop id in threadlocals?( process view )

      def process_view(self, request, view_func, view_args, view_kwargs)
          shop_prefix = view_kwargs.pop('shop_prefix', None):
          if shop_prefix:
              shop = Shop.objects.get(prefix=shop_prefix)
              request.shop = shop
              threadlocals.shop_pk = shop.pk
  • class-based views

  • class based plugins (not modules based!)

Plugin structure

Plugins should be class based, as most of the other stuff in Django is (for instace the admins), with the framework defining both a base class for plugin writers to extend, as well as a registration method for subclasses.

Proposal by fivethreeo for the plugin structure:

# django_shop/checkout/__init__.py
# django_shop/checkout/__init__.py


from django_shop.checkout.site import CheckoutSite, checkoutsite

def autodiscover():
    """
    Auto-discover INSTALLED_APPS admin.py modules and fail silently when
    not present. This forces an import on them to register any admin bits they
    may want.
    """

    import copy
    from django.conf import settings
    from django.utils.importlib import import_module
    from django.utils.module_loading import module_has_submodule

    for app in settings.INSTALLED_APPS:
        mod = import_module(app)
        for submod in ('django_shop_payment', 'django_shop_shipment')
            # Attempt to import the app's admin module.
            try:
                before_import_registry = copy.copy(checkoutsite._registry)
                import_module('%s.%s' % (app, submod))
            except:
                # Reset the model registry to the state before the last import as
                # this import will have to reoccur on the next request and this
                # could raise NotRegistered and AlreadyRegistered exceptions
                # (see #8245).
                site._registry = before_import_registry
    
                # Decide whether to bubble up this error. If the app just
                # doesn't have an admin module, we can ignore the error
                # attempting to import it, otherwise we want it to bubble up.
                if module_has_submodule(mod, submod):
                    raise

# django_shop/checkout/site.py

from djangoshop.payment_base import PaymentBase
from djangoshop.shipper_base import ShipperBase

from django.views.generic import TemplateView

class CheckoutView(TemplateView):
    template_name = "checkout.html"

class CheckoutSite(object):

    checkout_view = CheckoutView.as_view()
    
    def __init__(self, name=None, app_name='django_shop'):
        self._registry = {
            'shipper': {},
            'payment': {}
        }
         # model_class class -> admin_class instance
        self.root_path = None
        if name is None:
            self.name = 'checkout'
        else:
            self.name = name
        self.app_name = app_name
        
    def register(self, registry, classtype, class_or_iterable):
        """
        Registers the given model(s) with the checkoutsite
        """
        if isinstance(cls, classtype):
            class_or_iterable = [class_or_iterable]
        for cls in class_or_iterable:
            if cls in self._registry[registry].keys():
                raise AlreadyRegistered('The %s class %s is already registered' % (registry, cls.__name__))
            # Instantiate the class to save in the registry
            self._registry[registry][cls] = cls(self)

     def unregister(self, registry, classtype, class_or_iterable):
        """
        Unregisters the given classes(s).

        If a class isn't already registered, this will raise NotRegistered.
        """
        if isinstance(cls, classtype):
            class_or_iterable = [class_or_iterable]
        for cls in class_or_iterable:
            if cls not in self._registry[registry].keys():
                raise NotRegistered('The %s class %s is not registered' % (registry, cls.__name__))
            del self._registry[registry][cls] 
            
    def register_shipper(self, shipper):
        self.register(self, 'shipper', ShipperBase, shipper)
            
    def unregister_shipper(self, shipper):
        self.unregister(self, 'shipper', ShipperBase, shipper)
        
    def register_payment(self, payment):
        self.register(self, 'payment', PaymentBase, payment)
            
    def unregister_payment(self, payment):
        self.unregister(self, 'payment', PaymentBase, payment)
                
    def get_urls(self):
        from django.conf.urls.defaults import patterns, url, include

        # Checkout-site-wide views.
        urlpatterns = patterns('',
            url(r'^$', self.checkout_view, name='checkout'),
        )

        # Add in each model's views.
        for payment in self._payment_registry:
            if hasattr(payment, 'urls'):
                urlpatterns += patterns('',
                    url(r'^shipment/%s/%s/' % payment.url_prefix,
                        include(payment.urls))
                )
        for shipper in self._shippers_registry:
            if hasattr(shipper, 'urls'):
                urlpatterns += patterns('',
                    url(r'^payment/%s/' % payment.url_prefix,
                        include(shipper.urls))
                )
        return urlpatterns

    @property
    def urls(self):
        return self.get_urls(), self.app_name, self.name

checkoutsite = CheckoutSite()

# django_shop/checkout/shipper_base.py

class ShipperBase(object)
    pass
    
# django_shop/checkout/payment_base.py
from djangoshop. import RegisterAbleClass

class PaymentBase(object)

  def __init__(self, checkout_site):
    self.checkout_site = checkout_site
    super(PaymentBase, self).__init__()
    
# app/django_shop_shipment.py

from djangoshop.shipper_base import ShipperBase

class ShipmentClass(ShipperBase):

  def __init__(self, checkout_site):
    self.checkout_site = checkout_site
    super(PaymentBase, self).__init__()
    
checkoutsite.register_shipment(ShipmentClass)

# app/django_shop_payment.py

from django.views.generic import TemplateView
from djangoshop.payment_base import PaymentBase

class PaymentView(TemplateView):
    template_name = "payment.html"

class PaymentClass(PaymentBase, UrlMixin):
    
    url_prefix = 'payment'
    
    payment_view = PaymentView.as_view()
    
    def get_urls(self):
        from django.conf.urls.defaults import patterns, url

        urlpatterns = patterns('',
            url(r'^$', self.payment_view,
                name='%s_payment' % self.url_prefix),
        )
        return urlpatterns
        
    def urls(self):
        return self.get_urls()
    urls = property(urls)
    
checkoutsite.register_payment(PaymentClass)

Similar to the Django-CMS plugins, most of the shop plugins will probably have to render templates (for instance when they want to define a new checkout step).

Shoppping Cart

In its core this is a list of a kind of CartItems which relate to Product.

It should be possible to have the same Product in different CartItems when some details are different. Stuff like different service addons etc.

Prices

This seems to be rather complex and must be pluggable. Prices may be influenced by many different things like the Product itself, quantities, the customer (location, special prices), shipping method and the payment method. This all would have to be handled by special / custom pricing implementations. The core implementation must only include ways for such extension possibilities.

Prices will also be related to taxes in some way.

The core product implementation should possibly know nothing about prices and taxes at all.

Table Of Contents

This Page