svv/orders.py
author Tero Marttila <terom@fixme.fi>
Thu, 23 Dec 2010 00:57:46 +0200
changeset 9 0327b83959e9
parent 7 bbac4b0f4320
child 10 4bdb45071c89
permissions -rw-r--r--
Implement working NewOrderView, and start restructuring it again right away...
# coding: utf-8
"""
    Order data model/view/handler
"""

from svv.controllers import PageHandler
from svv.html import tags
from svv import database as db

import datetime
import logging

log = logging.getLogger('svv.orders')

class FormError (Exception) :
    """
        A user-level error in a form field
    """

    def __init__ (self, field, error) :
        """
                field       - name of field with error
                error       - descriptive text for user
        """

        self.field = field

        super(FormError, self).__init__(error)

class OrderForm (object) :
    """
        A single instance of a <form>, where we can process submitted data from the client, storing the associated
        Order-related data, and then render a form for any of that related data.
    """
    
    # any POST data we have processed, updated from process()
    data = None

    def __init__ (self, app) :
        """
                app             - bind this form to the app state (db etc)
        """

        self.app = app

    def defaults (self) :
        """
            Update our attributes with default values
        """

        self.customer_id = None
        self.customer_name = None

        self.contact_id = None
        self.contact_name = None
        self.contact_phone = None
        self.contact_email = None
        self.contact_customer = None

        self.event_name = None
        self.event_description = None

        tomorrow = datetime.date.today() + datetime.timedelta(days=1)
        
        # default to tomorrow afternoon
        self.event_start = datetime.datetime.combine(tomorrow, datetime.time(16, 00))
        
        # automatically determined once start is set
        self.event_end = None

    def process_raw_field (self, name, default=None, required=None) :
        """
            Process a generic incoming data field.

                default         - value to return if no value was present
                required        - raise a FormError if no value present

            Returns the value as a str, or default
        """

        if name in self.post :
            return self.post[name]

        elif required :
            raise FormError(name, "Required field")

        else :
            return default

    def process_string_field (self, name, default=None, required=None, strip=True) :
        """
            Process a generic incoming string field.

            Trims extra whitespace from around the value, unless strip=False is given.

            Returns the value as unicode, or default.
        """

        value = self.process_raw_field(name, required=required)

        if value is None :
            return default

        try :
            # XXX: decode somehow, or can werkzeug handle that?
            value = unicode(value)

        except UnicodeDecodeError :
            raise FormError(name, "Failed to decode Unicode characters")

        if strip :
            value = value.strip()

        return value
    
    def process_integer_field (self, name, default=None, required=None) :
        """
            Process a generic incoming int field.

            Returns the value as int, or default.
        """

        value = self.process_raw_field(name, required=required)

        if value is None :
            return default

        try :
            return int(value)

        except ValueError :
            raise FormError(name, "Must be a number")
    
    DATETIME_FORMAT = "%d.%m.%Y %H:%M"

    def process_datetime_field (self, name, default=None, required=None, format=DATETIME_FORMAT) :
        """
            Process an incoming datetime field.

            Returns the value as datetime, or default.
        """

        value = self.process_raw_field(name, required=required)

        if value is None :
            return default

        try :
            return datetime.datetime.strptime(value, format)

        except ValueError, ex :
            raise FormError(name, "Invalid date/time value: " + str(ex))

    def process_multifield (self, table, id, fields) :
        """
            Process a set of user-given field values for an object with an unique id, and some set of additional fields.
            
            If the id is given, look up the corresponding field values, and return those.

            If any of the fields are given, either look up a matching row, or create a new one, and return its id.

            Returns an (id_name, field_name, ...) N-tuple.
        """

        id_name, id_col, id_value = id

        if id_value :
            # look up object from db
            columns = [col for name, col, value in fields]

            sql = db.select(columns, (id_col == id_value))

            for row in sql :
                # XXX: sanity-check row values vs our values

                # new values
                fields = tuple(
                    (name, col, row[col]) for name, col, value in fields
                )
                
                # ok, just use the first one
                break

            else :
                # not found!
                raise FormError(id_name, "Item selected does not seem to exist")
                
            log.info("Lookup %s=%d -> %s", id_name, id_value, dict((name, value) for name, col, value in fields))

        elif any(value for name, col, value in fields) :
            # look up identical object from db?
            sql = db.select([id_col], db.and_(*[(col == value) for name, col, value in fields]))

            for row in self.app.query(sql) :
                if id_value :
                    log.warn("Duplicate %s=%d for %s", id_name, id_value, dict((name, value) for name, col, value in fields))
                
                # found object's id
                id_value = row[id_col]

                log.info("Found %s -> %d", dict((name, value) for name, col, value in fields), id_value)
            
            # create new object?
            if not id_value :
                sql = db.insert(table).values(dict((col, value) for name, col, value in fields))

                id_value, = self.app.insert(sql)

                log.info("Create %s -> %d", dict((name, value) for name, col, value in fields), id_value)

        else :
            # not known
            log.debug("No %s known for order...", id_name)
        
        # return full set of values
        return (id_value, ) + tuple(value for name, col, value in fields)

    def process_customer (self) :
        """
            Process the incoming customer_* fields, returning (customer_id, customer_name).
        """

        return self.process_multifield(db.customers,
            ('customer_id', db.customers.c.id, self.process_integer_field('customer_id')),
            (
                ('customer_name', db.customers.c.name, self.process_string_field('customer_name')),
            ),
        )

    def process_contact (self, customer_id) :
        """
            Process the incoming contact_* fields, returning 
                (contact_id, contact_name, contact_phone, contact_email, contact_customer)
        """

        return self.process_multifield(db.contacts,
            ('contact_id', db.contacts.c.id, self.process_integer_field('contact_id')),
            (
                ('contact_name', db.contacts.c.name, self.process_string_field('contact_name')),
                ('contact_phone', db.contacts.c.phone, self.process_string_field('contact_phone')),
                ('contact_email', db.contacts.c.email, self.process_string_field('contact_email')),
                ('contact_customer', db.contacts.c.customer, customer_id),
            ),
        )

    def process_event (self) :
        """
            Process the incoming event_* fields, returning
                (event_name, event_description, event_start, event_end)
        """

        event_name = self.process_string_field('event_name')
        event_description = self.process_string_field('event_description', strip=False)
        event_start = self.process_datetime_field('event_start')
        event_end = self.process_datetime_field('event_end')

        return (event_name, event_description, event_start, event_end)

    def process (self, data) :
        """
            Bind ourselves to the given incoming POST data, and update our order field attributes.

                data        - the submitted POST data as a MultiDict
        """

        # bind the raw post data
        self.data = data

        # customer
        self.customer_id, self.customer_name = self.process_customer()

        # contact
        self.contact_id, self.contact_name, self.contact_phone, self.contact_email, self.contact_customer = self.process_contact(customer_id)

        if self.contact_customer and not self.customer_id :
            # TODO: be really smart?
            pass

        # event
        self.event_name, self.event_description, self.event_start, self.event_end = self.process_event()


    def build_customer_list (self) :
        """
            Query a (id, name) list of customers.
        """

        sql = db.select([db.customers.c.id, db.customers.c.name])

        return self.app.query(sql)

    def build_contact_list (self, customer_id=None) :
        """
            Query a (id, name, phone, email) list of contacts, optionally for given customer if given.
        """

        sql = db.select([db.contacts.c.id, db.contacts.c.name, db.contacts.c.phone, db.contacts.c.email])

        if customer_id :
            sql = sql.where((db.contacts.c.customer == customer_id))

        return self.app.query(sql)


    def render_text_input (self, name, value=None, multiline=False) :
        """
            Render HTML for a generic text field input.
                
                name            - field name, as used for POST
                value           - default field value
                multiline       - use a multi-line <textarea> instead
        """

        if multiline :
            # XXX: textarea can't be self-closing for some reason?
            return tags.textarea(name=name, id=name, _selfclosing=False, _whitespace_sensitive=True)(value)

        else :
            return tags.input(type='text', name=name, id=name, value=value)

    def render_select_input (self, name, options, value=None) :
        """
            Render HTML for a generic select control.

                name            - field name, as used for POST
                options         - sequence of (value, title) options. `value` may be None to omit.
                value           - the selected value
        """

        return tags.select(name=name, id=name)(
            (
                tags.option(value=opt_value, selected=('selected' if opt_value == value else None))(opt_title)
            ) for opt_value, opt_title in options
        )

    def render_customer_input (self) :
        """
            Render HTML for customer_id/name field inputs.
        """

        # all known customers
        customers = self.build_customer_list()
        
        return (
            self.render_select_input('customer_id', customers, self.customer_id),
            self.render_text_input('customer_name', self.customer_name),

            tags.script(r"$(document).ready(function () { $('#customer_id').formSelectPreset({textTarget: $('#customer_name')}); });"),
        )

    def render_contact_input (self) :
        """
            Render HTML for contact name field <input>s
        """
        # recommended contacts for selected customer, if known
        contacts = self.get_contact_list(self.customer_id)

        return (
            self.render_select_input('contact_id', ((id, name) for id, name, phone, email in contacts), self.contact_id)
            self.render_text_input('contact_name', self.contact_name),

            tags.script(r"$(document).ready(function () { $('#contact_id').formSelectPreset({textTarget: $('#contact_name')}); });"),
        )

    def render_event_input (self) :
        """
            Render HTML for event start/end field <input>s
        """
        
        return (
            self.render_text_input('event_start', (self.event_start.strftime(self.DATETIME_FORMAT) if event_start else None)),
            " - ",
            self.render_text_input('event_end', (self.event_end.strftime(self.DATETIME_FORMAT) if event_end else None)),

            tags.script(r"""
$(document).ready(function () { 
    var event_start = $('#event_start');
    var event_end = $('#event_end');

    event_start.datetimepicker();
    event_end.datetimepicker(); 
    
/* Buggy shit doesn't work

    {
        beforeShow: function (input, inst) {
            // copy default value from event_start
            event_end.datetimepicker("option", "defaultDate", event_start.datetimepicker("getDate"));
        }
    }   

    event_start.change(function () {
        // copy value as default
        var start_date = event_start.datetimepicker("getDate");

        event_end.datetimepicker("option", "defaultDate", start_date);
    });

    // init default as well
    event_start.change();
*/
});"""      ),

        )

    def render_form_field (self, name, title, description, inputs) :
        """
            Render the label, input control, error note and description for a single field, along with their containing <li>.
        """

        return tags.li(class_='field')(
            tags.label(title, for_=name),

            inputs,

            # XXX: somewhere where we tag these!
            # tags.span("Error!"),

            tags.p(description),
        )


    def render (self, action, submit=u"Tallenna") :
        """
            Render the entire <form>, using any loaded/processed values.

                action          - the target URL for the form to POST to
                submit          - label for the submit button
        """

        return tags.form(action=action, method='POST')(
            tags.fieldset(
                tags.legend(u"Tilaaja"),
                
                tags.ol(
                    self.render_form_field('customer_name', u"Tilaaja", u"Tilaavan yhdistyksen/henkilön nimi", (
                        self.render_customer_input()
                    )),

                    self.render_form_field('contact_name', u"Yhteyshenkilö", u"Yhteyshenkilön nimi, jos eri kun tilaaja", (
                        self.render_contact_input()
                    )),

                    self.render_form_field('contact_phone', u"Puhelin", u"Yhteyshenkilön puhelinnumero", (
                        self.render_text_input('contact_phone', self.contact_phone)
                    )),

                    self.render_form_field('contact_email', u"Sähköposti", u"Yhteyshenkilön sähköpostiosoite", (
                        self.render_text_input('contact_email', self.contact_email)
                    )),
                ),
            ),

            tags.fieldset(
                tags.legend(u"Tapahtuma"),
           
                tags.ol(
                    self.render_form_field('event_name', u"Tapahtuma", u"Tapahtuman lyhyt nimi", (
                        self.render_text_input('event_name', self.event_name)
                    )),

                    self.render_form_field('event_description', u"Lisätiedot", u"Tapahtuman tarkemmat tiedot", (
                        self.render_text_input('event_description', self.event_description, multiline=True)
                    )),

                    self.render_form_field('event_start', u"Ajankohta", u"Tapahtuman ajankohta (kamat noudetaan - palautetaan)", (
                        self.render_event_input()
                    )),
                ),
            ),

            tags.input(type='submit', value=submit),
        )


class OrdersView (PageHandler) :
    def render (self) :
        return tags.h1("Orders list")

class OrderView (PageHandler) :
    def render (self, id) :
        return tags.h1("Order info for #%d" % (id, ))

class NewOrderView (PageHandler) :
    """
        
    """

    def render (self) :
        if self.POST :
            print self.POST

            # 
            
            # if we've gotten this far, then we can create it!
            sql = db.insert(db.orders).values(
                customer            = customer_id,
                contact             = contact_id,
                
                event_name          = event_name,
                event_description   = event_description,
                event_start         = event_start,
                event_end           = event_end,
            )

            # go!
            order_id, = self.app.insert(sql)

            # ok, we don't need the /new URL anymore, we can just show the order page
            return self.redirect_for(OrderView, id=order_id)

        # render form
        return self.render_form()