import posixpath as unixpath import myghty.component as component import myghty.escapes as escapes import re, os import shoppingdata, shoppingmodel, form from statemachine import * import highlight # shopping cart commands CMD_ADD = 'add' CMD_UPDATE = 'update' CMD_REMOVE = 'remove' CMD_NEXT = 'next' CMD_PREVIOUS = 'previous' # checkout process states # the numbers are so they can be compared for ordering CHECKOUT_START = '1_start' CHECKOUT_BILLING = '2_billing' CHECKOUT_SHIPPING = '3_shipping' CHECKOUT_PAYMENT = '4_payment' CHECKOUT_CONFIRM = '5_confirm' CHECKOUT_DONE = '6_done' ## module components ## class _Store(object): def do_component_init(self, component): self.store_path = component.interpreter.attributes.get('store_path') def catalog(self, m): """catalog component, parses the path and displays product list pages""" app = _RequestHelper(m, self) match = re.match(r".*/catalog/(.*/)?$", m.request_path) if not match: m.abort(404) path = match.group(1) if path is not None: path = escapes.url_unescape(path) category = shoppingdata.store if path: try: category = category.get_category('/' + path) except KeyError: m.abort(404) if len(category.items) > 0: items = category.items else: items = shoppingdata.featureditems.items app.show_template("catalog.myt", category = category, items = items) def item(self, m): """item component, parses the path and displays individual item pages""" match = re.match(r".*/item/(.*)/?$", m.request_path) itemname = match.group(1) if itemname is not None: itemname = escapes.url_unescape(itemname) try: item = shoppingdata.store.get_item(itemname) except KeyError: m.abort(404) _RequestHelper(m, self).show_template("item.myt", category = item.primarycategory, item = item) def cart(self, m, ARGS, cmd = None, qty = None, itemname = None, index = None): """cart component, displays the shopping cart and handles updates""" app = _RequestHelper(m, self) save = False user = app.get_user() if user.cart is None: user.cart = shoppingmodel.Cart() save = True if itemname is not None: item = shoppingdata.store.get_item(itemname) variants = [] for category in item.get_variant_categories(): variants.append(item.get_variant(category, ARGS['variant_' + category])) if cmd == CMD_ADD: try: user.cart.add_item(item, int(qty), variants) save = True except ValueError: pass elif cmd == CMD_UPDATE: for i in range(0, len(user.cart.items)): try: user.cart.items[i].quantity = int(ARGS['qty_' + str(i)]) except ValueError: pass save = True elif cmd == CMD_REMOVE: try: user.cart.remove_item(int(index)) save = True except ValueError: pass if save: app.save_session() app.show_template("cart.myt", cart=user.cart) def checkout(self, m, ARGS, cmd = None, **params): """manages the checkout process, including form field handling and state transition. Delegates most of the work onto a CheckoutTransitions object which handles state transitions.""" app = _RequestHelper(m, self) smachine = _CheckoutTransitions(app) smachine.do_transition(cmd) if smachine.template is not None: app.save_session() app.show_template(smachine.template, ck_state = smachine.state, form = smachine.form, invoice = smachine.invoice) def source(self, m, r): """view source component, opens the source file up and displays. clearly, one should be careful with the configuration of this type of component lest it be too flexible in what it shows. """ filename = r.filename if os.path.isdir(filename): m.abort(403) try: f = file(filename) except IOError: m.abort(404) r.content_type = 'text/html' s = f.read() s = highlight.highlight(s, filename = filename) _RequestHelper(m, self).show_template("viewsource.myt", name = m.request_path, source = s) index = _Store() class _RequestHelper(object): """a per-request helper object that performs common functions using the request object as well as the session.""" def __init__(self, m, handler): self.m = m self.handler = handler session = m.get_session() if not session.has_key('shopping_user'): user = shoppingmodel.User() session['shopping_user'] = user session.save() def get_user(self): return self.m.get_session()['shopping_user'] def save_session(self): self.m.get_session().save() def show_template(self, template, **params): self.m.subexec(self.handler.store_path + '/' + template, **params) class _CheckoutTransitions(StateMachine): """ a StateMachine implementation storing all the possible transitions for the shopping cart. the state itself is stored in the session and is also matched against a hidden form variable. if the two don't match, no transition occurs. this approach insures that the state of the checkout is determined by the server and cannot be overridden by a fabricated HTTP request. It also renders any errors related to the "reload" button irrelevant. users can go back/next/reload till the sun comes down on any checkout page and it wont screw up their state/submit twice/etc. """ def __init__(self, app): self.app = app self.ARGS = app.m.request_args self.user = app.get_user() self.invoice = None self.session = app.m.get_session() def create_form(self): """returns a Form object representing all the fields we are going to collect. the Form is stored in the users session.""" return form.Form('checkout', [ form.FormField('ck_state', default=CHECKOUT_START), form.FormField('currentform'), form.SubForm('billing', [ form.IntFormField('useaddress', options = []), form.FormField('firstname', description = "First Name", required=True), form.FormField('lastname', description = "Last Name", required=True), form.FormField('street1', description = "Street", required=True), form.FormField('street2', description = "Street"), form.FormField('city', description = "City", required=True), form.FormField('state', description = "State", required=True), form.FormField('zipcode', description = "Zip Code", required=True), form.FormField('country', descriptinon = "Country", required=True, default='USA'), ]), form.SubForm('shipping', [ form.IntFormField('useaddress', options = []), form.FormField('firstname', description = "First Name", required=True), form.FormField('lastname', description = "Last Name", required=True), form.FormField('street1', description = "Street", required=True), form.FormField('street2', description = "Street"), form.FormField('city', description = "City", required=True), form.FormField('state', description = "State", required=True), form.FormField('zipcode', description = "Zip Code", required=True), form.FormField('country', descriptinon = "Country", required=True, default='USA'), ]), form.SubForm('payment', [ form.FormField('ccname', description="Credit Card Name", required = True, textsize=40), form.FormField('cctype', description = "Credit Card Type", required = True, options=( ('amex', 'American Express'), ('visa', 'Visa'), ('mastercard', 'Master Card'), )), form.CCFormField('ccnumber', description ="Credit Card Number", required = True, textsize=20, default = '371449635398431'), form.DateFormField('ccexp', description ="Credit Card Expiration Date", required = True, fields = ['month', 'year'], yeardeltas = range(0, 9)), ]), ]) def do_transition(self, transition): if not self.session.has_key('invoice_form') or transition is None: self.state = CHECKOUT_START self.invoice_form = self.create_form() self.session['invoice_form'] = self.invoice_form else: self.invoice_form = self.session['invoice_form'] self.state = self.invoice_form.elements['ck_state'].displayvalue self.set_current_form(self.invoice_form.elements['currentform'].displayvalue) if self.state >= CHECKOUT_CONFIRM: self.template = 'confirm.myt' self.invoice = self.user.invoice else: self.template = 'checkout.myt' if self.state != self.ARGS['ck_state'] or self.state == CHECKOUT_DONE: return if transition is None: transition = CMD_NEXT try: self.state = StateMachine.do_transition(self, self.state, transition) self.invoice_form.elements['ck_state'].set_value(self.state) except AbortTransition: pass def set_current_form(self, name): self.invoice_form.elements['currentform'].set_value(name) self.form = self.invoice_form.elements[name] def set_address_dropdowns(self): addresses = [(i, self.user.addresses[i].street1) for i in range(0, len(self.user.addresses))] if len(addresses): addresses.insert(0, ('', 'select an address')) self.invoice_form.elements['billing'].elements['useaddress'].options = addresses self.invoice_form.elements['shipping'].elements['useaddress'].options = addresses def extract_address(self): address = shoppingmodel.Address() self.form.reflect_to(address) for a in self.user.addresses: if address == a: break else: self.user.addresses.append(address) self.set_address_dropdowns() return address def fill_address(self): address = self.user.addresses[self.form.elements['useaddress'].currentvalue] self.form.elements['useaddress'].set_value(None) self.form.reflect_from(address) self.form.unvalidate() return address def start_to_billing(self): self.set_address_dropdowns() self.set_current_form('billing') self.template = 'checkout.myt' def billing_to_start(self): self.app.show_template("cart/") self.template = None def billing_to_shipping(self): self.form.set_request(self.ARGS) if self.form.elements['useaddress'].currentvalue is not None: address = self.fill_address() self.set_current_form('shipping') self.form.unvalidate() elif self.form.is_valid(): self.extract_address() self.set_current_form('shipping') self.form.unvalidate() else: raise AbortTransition() def shipping_to_billing(self): self.set_current_form('billing') self.form.unvalidate() def shipping_to_payment(self): self.form.set_request(self.ARGS) if self.form.elements['useaddress'].currentvalue is not None: address = self.fill_address() ccname = address.firstname + " " + address.lastname self.set_current_form('payment') self.form.elements['ccname'].set_value(ccname) self.form.unvalidate() elif self.form.is_valid(): address = self.extract_address() ccname = address.firstname + " " + address.lastname self.set_current_form('payment') self.form.elements['ccname'].set_value(ccname) self.form.unvalidate() else: raise AbortTransition() def payment_to_shipping(self): self.set_current_form('shipping') self.form.unvalidate() def payment_to_confirm(self): self.form.set_request(self.ARGS) if self.form.is_valid(): billing= shoppingmodel.Address() self.invoice_form.elements['billing'].reflect_to(billing) shipping = shoppingmodel.Address() self.invoice_form.elements['shipping'].reflect_to(shipping) cc = shoppingmodel.CreditCard() self.invoice_form.elements['payment'].reflect_to(cc) invoice = shoppingmodel.Invoice(self.user.cart.items, self.user, billing, shipping, cc) self.user.invoice = invoice self.invoice = invoice self.template = 'confirm.myt' else: raise AbortTransition() def confirm_to_payment(self): self.set_current_form('payment') self.template = 'checkout.myt' self.form.unvalidate() def confirm_to_done(self): self.invoice = self.user.invoice self.user.cart.items = [] self.template = 'confirm.myt' transitions = dict([ Transition(CHECKOUT_START, CHECKOUT_BILLING, CMD_NEXT, start_to_billing), Transition(CHECKOUT_BILLING, CHECKOUT_START, CMD_PREVIOUS, billing_to_start), Transition(CHECKOUT_BILLING, CHECKOUT_SHIPPING, CMD_NEXT, billing_to_shipping), Transition(CHECKOUT_SHIPPING, CHECKOUT_BILLING, CMD_PREVIOUS, shipping_to_billing), Transition(CHECKOUT_SHIPPING, CHECKOUT_PAYMENT, CMD_NEXT, shipping_to_payment), Transition(CHECKOUT_PAYMENT, CHECKOUT_SHIPPING, CMD_PREVIOUS, payment_to_shipping), Transition(CHECKOUT_PAYMENT, CHECKOUT_CONFIRM, CMD_NEXT, payment_to_confirm), Transition(CHECKOUT_CONFIRM, CHECKOUT_PAYMENT, CMD_PREVIOUS, confirm_to_payment), Transition(CHECKOUT_CONFIRM, CHECKOUT_DONE, CMD_NEXT, confirm_to_done), ])