This document should gather all the pre-code architecture requirements/research.
Generally, the shop system can be seen as two different phases, with two different problems to solve:
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.
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”.
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!)
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).
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.
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.