Split wsgi.py into controllers/customers/urls for now; start orders form
authorTero Marttila <terom@fixme.fi>
Wed, 22 Dec 2010 02:52:25 +0200
changeset 6 72c73df76db2
parent 5 c72e0314b930
child 7 bbac4b0f4320
Split wsgi.py into controllers/customers/urls for now; start orders form
bin/wsgi-dev.py
static/forms.css
static/js/forms.js
svv/application.py
svv/controllers.py
svv/customers.py
svv/database.py
svv/orders.py
svv/urls.py
svv/wsgi.py
--- a/bin/wsgi-dev.py	Wed Dec 22 02:51:41 2010 +0200
+++ b/bin/wsgi-dev.py	Wed Dec 22 02:52:25 2010 +0200
@@ -6,7 +6,9 @@
 import werkzeug
 
 # app import
-from svv import wsgi
+import svv.wsgi
+import svv.database
+import svv.application
 
 import optparse, logging
 
@@ -15,9 +17,13 @@
     parser = optparse.OptionParser()
     parser.add_option('-q', '--quiet', action='store_true', help='More output')
     parser.add_option('-v', '--verbose', action='store_true', help='More output')
+
     parser.add_option('-p', '--port', type='int', help='Local port to run on', default=8080, metavar='PORT')
     parser.add_option('-B', '--bind', help="Local address to listen on", default='localhost', metavar='HOST')
 
+    parser.add_option('-d', '--database', help="Database connection URI", metavar='URL')
+    parser.add_option(      '--init-database', action='store_true', help="Initialize database (CREATE)")
+
     (options, args) = parser.parse_args()
     
     if options.quiet :
@@ -37,12 +43,22 @@
 
     logging.basicConfig(format="[%(levelname)5s] %(funcName)25s : %(message)s", level=level)
 
-    app = wsgi.WSGIApp(
-            # params
+    # app state
+    application = svv.application.Application(
+            options.database,
+    )
+
+    # init?
+    if options.init_database :
+        application.create_tables()
+
+    # frontend
+    wsgiapp = svv.wsgi.WSGIApp(
+            application
     )
 
     # run
-    werkzeug.run_simple(bind, port, app, use_reloader=True, use_debugger=True, 
+    werkzeug.run_simple(bind, port, wsgiapp, use_reloader=True, use_debugger=True, 
             static_files    = {
                 # static resources mounted off app /static
                 '/static':  'static/',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/static/forms.css	Wed Dec 22 02:52:25 2010 +0200
@@ -0,0 +1,88 @@
+/*
+ * Form layout
+ *
+ * Inspiration taken from http://articles.sitepoint.com/article/fancy-form-design-css
+ */
+
+fieldset
+{
+    /* A fieldset will not be completely full-width, and will be vertically separated from adjacent fieldsets*/
+    margin: 1em 20% 1em 0em;
+
+    /* A fieldset shall not have any padding of its own, as we position the elements iniside it */
+    padding: 0em;
+
+    border: thin solid #aaa;
+}
+
+/* A fieldset's legend will look similar to a h2 element, except included as part of the frame */
+fieldset legend
+{
+    width: 100%;
+
+    font-size: large;
+    font-weight: bold;
+
+    margin: 0em 1em;
+    padding: 0.5em;
+
+    background-color: #e5e5e5;
+
+    border: 1px dashed #c5c5c5;
+}
+
+/* A fieldset will be internally structured using a list, that is not itself visible */
+fieldset ol
+{
+    list-style-type: none;
+
+    margin: 0px;
+    
+    /* Recreate padding removed from fieldset */
+    padding: 0em 1em;
+
+    width: 100%;
+}
+
+/* Fields inside a fieldset will be vertically separated */
+fieldset ol li
+{
+    padding: 1em 1em 0em;
+
+    border-top: 1px solid #c5c5c5;
+}
+
+fieldset ol li:first-child
+{
+    border-top: none;
+}
+
+/* The field identifier displays above the field itself, and is visually weighted, but smaller than the fieldset legend */
+fieldset label
+{
+    display: block;
+    
+    font-weight: bold;
+    font-size: small;
+}
+
+/* The field element are consistently aligned */
+form input,
+form textarea
+{
+    width: 40%;
+
+    border: thin solid #444;
+    padding: 4px;
+}
+
+/* Field's descriptive text */
+fieldset p
+{
+    margin: 0.5em;
+
+    font-style: italic;
+    font-size: x-small;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/static/js/forms.js	Wed Dec 22 02:52:25 2010 +0200
@@ -0,0 +1,61 @@
+/**
+ * jQuery support code for our forms
+ */
+
+(function($) {
+    /*
+     * When non-zero <select>/<option> is selected, apply that option as pre-filled values for other form items
+     */
+    $.fn.formSelectPreset = function (opts) {
+        opts = $.extend({
+            // which option value is the "default", i.e. 'create new'
+            valueDefault: 0,
+
+            // which element to apply selected option value (id) to
+            valueTarget: null,
+
+            // which element to apply selected option text to
+            textTarget: null,
+        }, opts);
+
+        this.change(function (event) {
+            // selected option value (i.e. id)
+            value = $(this).val();
+
+            // selected option text (i.e. title/name)
+            text = $.trim($(this).find("option:selected").text());
+
+            // fields to set
+            field_values = [
+                [ opts.valueTarget, value ],
+                [ opts.textTarget, text ]
+            ];
+
+            if (value == opts.valueDefault) {
+                // clear and re-enable fields
+                if (opts.valueTarget) {
+                    opts.valueTarget.removeAttr('disabled');
+                    opts.valueTarget.val("");
+                }
+
+                if (opts.textTarget) {
+                    opts.textTarget.removeAttr('disabled');
+                    opts.textTarget.val("");
+                }
+
+                return;
+            }
+
+            // set field values
+            if (opts.valueTarget) {
+                opts.valueTarget.attr('disabled', "disabled");
+                opts.valueTarget.val(value);
+            }
+
+            if (opts.textTarget) {
+                opts.textTarget.attr('disabled', "disabled");
+                opts.textTarget.val(text);
+            }
+        });
+    }
+})(jQuery);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svv/application.py	Wed Dec 22 02:52:25 2010 +0200
@@ -0,0 +1,52 @@
+"""
+    Application runtime state
+"""
+
+from sqlalchemy import create_engine
+
+import logging
+
+from svv import database as db
+
+log = logging.getLogger('svv.app')
+
+class Application (object) :
+    """
+        Our core run-time state, which e.g. WSGIAoo acts as a frontend to
+    """
+
+    def __init__ (self, db_url) :
+        """
+            Initialize app, connecting to database.
+
+                db_url      - sqlalchemy-style URL to database
+        """
+        
+        if db_url :
+            # connect
+            self.engine = create_engine(db_url)
+
+            log.info("Connected to database %s", self.engine)
+
+        else :
+            self.engine = None
+
+    def create_tables (self) :
+        """
+            Initialize the database if new.
+        """
+
+        log.warn("Creating database tables...")
+
+        db.schema.create_all(self.engine)
+
+    def get_connection (self) :
+        """
+            Return an active sqlalchemy Connection for our database.
+        """
+        
+        if not self.engine :
+            raise RuntimeError("No database configured")
+
+        return self.engine.connect()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svv/controllers.py	Wed Dec 22 02:52:25 2010 +0200
@@ -0,0 +1,243 @@
+# coding: utf-8
+"""
+    Generic controller base classes.
+
+    respond(Request) -> Response
+"""
+
+from werkzeug import Response
+
+from svv import html
+from svv.html import tags
+
+class AppHandler (object):
+    """
+        Per-request handler type, containing the request context and implementing the response.
+
+        Works on a generic level, i.e. WSGI/Werkzeug URL-mapped request in, response out.
+    """
+
+    def __init__ (self, app, request, urlmap) :
+        """
+            Initialize for processing the given Request, to prepare for action-method later on
+
+                app     - the global Application state
+                request - the request state
+                urlmap  - the URL map we were matched through
+        """
+
+        self.app = app
+        self.request = request
+        self.urlmap = urlmap
+
+    def build_url (self, endpoint, **values) :
+        """
+            Return an URL for the given endpoint, applying the given values
+        """
+
+        return self.urlmap.build(endpoint, values)
+
+    def respond (self, url_values) :
+        """
+            Handle request that was mapped to ourselves via the URL routing, using given dict of values from URL.
+        """
+
+        raise NotImplementedError()
+
+class PageHandler (AppHandler) :
+    """
+        Specialized AppHandler for normal HTML page views.
+
+        Renders the layout template and HTML.
+    """
+
+    def respond (self, url_values) :
+        title = "Index"
+        css = [
+                "/static/layout.css", 
+                "/static/style.css", 
+                "/static/forms.css",
+                "/static/treelist.css"
+        ]
+        scripts = [
+                "/static/js/jquery.js",
+                "/static/js/forms.js",
+                #"/static/js/prototype.js", 
+                #"/static/js/scriptaculous/scriptaculous.js", 
+                #"/static/js/treelist.js"
+        ]
+
+        head = (
+            tags.title(title),
+            (tags.link(rel='Stylesheet', type="text/css", href=src) for src in css),
+            # XXX: script can't self-close >_<
+            (tags.script(src=src)(" ") for src in scripts),
+        )
+
+        # XXX: silly circular-import hack for urls
+        #      it's not sustainable for a base class to referr to its subclasses at define time
+        from svv import urls
+
+        header = ("Foo List")
+        nav = [tags.ul(tags.li(tags.a(href='#')(name)) for name in ['Nav A', 'Nav B', 'Nav C'])]
+        menu = [tags.ul(tags.li(tags.a(href=url)(name)) for name, url in [
+            ("Etusivu",     self.build_url(urls.Index)),
+            ("Uusi tilaus", self.build_url(urls.NewOrderView)),
+            ("Tilaukset",   self.build_url(urls.OrdersView)),
+            ("Tilaajat",    self.build_url(urls.CustomersView)),
+        ])]
+        footer = ("Copyright?")
+
+        # render page HTML
+        content = self.render(**url_values)
+        
+        layout = (
+            tags.div(id='header')(header),
+            tags.div(id='container')(
+                tags.div(id='menu')(menu),
+                tags.div(id='nav')(nav),
+                tags.div(id='content')(content),
+            ),
+            tags.div(id='footer')(footer),
+        )
+        
+        # perform the actual rendering (run generators etc.)
+        html_text = unicode(html.document(head, layout))
+        
+        # response object
+        # XXX: unicode?
+        return Response(html_text, mimetype='text/html')
+
+    def render (self, **args) :
+        """
+            Render and return HTML for page content.
+
+            XXX: must be HTML object, support just text?
+        """
+
+        raise NotImplementedError()
+
+
+
+### XXX: random test controllers
+
+class Document (AppHandler) :
+    """
+        PDF generation/export
+    """
+
+    def respond (self, url_values) :
+
+        title = url_values.get('name', "Hello World")
+
+        tpl = pdf.PageTemplate('id', 
+            header_columns  = (
+                ("", ""),
+                ("", ""),
+                ("", ""),
+                ("Vuokrasopimus", "%(today)s\n" + title + "\n"),
+            ),
+            footer_columns  = (
+                ("Teekkarispeksi Ry", "www.teekkarispeksi.fi"),
+                ("Tekniikkavastaava", "Juha Kallas\n045 xxx yyzz\njskallas@cc.hut.fi"),
+                ("Varastovastaava", "Joel Pirttimaa\n045 xxx yyzz\njhpirtti@cc.hut.fi"),
+                ("", ""),
+            ),
+        )
+
+        doc = pdf.DocumentTemplate([tpl],
+            title = title, author = "A. N. Onous"
+        )
+
+        # stylesheet
+        styles = pdf.Styles
+
+        # tree root
+        list_seq = pdf.ListItem.seq
+        tree = pdf.ListItem("Sopimusehdot", styles.h2, None, list_seq(), [
+            pdf.ListItem("Osapuolet", styles.list_h2, None, list_seq(), [
+                pdf.ListItem(None, None, "Teekkarispeksi ry (Y-tunnus 1888541-7), jäljempänä “Vuokranantaja”."),
+                pdf.ListItem(None, None, title + u", jäljempänä “Vuokraaja”. 1.1 ja 1.2 jäljempänä yhdessä “osapuolet”.")
+            ]),
+            pdf.ListItem("Kaluston lainaaminen", styles.list_h2, None, list_seq(), [
+                pdf.ListItem("Yleistä", styles.list_h3, "Tässä sopimuksessa sovitaan toiminnasta Vuokranantajan lainatessa tanssimattoja Vuokraajalle"),
+                pdf.ListItem("Vuokranantajan velvollisuudet", styles.list_h3, "Vuokranantaja sitoutuu toimittamaan sovittuna ajankohtana Vuokraajalle erikseen sovittava (liite) määrä tanssimattoja."),
+                pdf.ListItem("Blaa Blaa", styles.list_h3, "Etc."),
+            ]),
+            pdf.ListItem("Tätä sopimusta ja sitä koskevia erimielisyyksiä koskevat määräykset", styles.list_h2, None, list_seq(), [
+                pdf.ListItem("Sopimuksen voimassaolo", styles.list_h3, "Sopimus on osapuolia sitova sen jälkeen, kun osapuolet ovat sen allekirjoittaneet."),
+                pdf.ListItem("Muutosten tekeminen", styles.list_h3, "Muutokset sopimukseen on tehtävä kirjallisesti molempien osapuolten kesken."),
+                pdf.ListItem("Blaa Blaa", styles.list_h3, "Etc."),
+            ]),
+        ])
+
+        from reportlab.platypus import Paragraph as p
+
+        elements = [
+                p("Vuokrasopimus", styles.h1),
+                p("Teekkarispeksi ry AV-tekniikka", styles.h3),
+        ] + list(tree.render_pdf()) + [
+                p("Nouto", styles.h2),
+                p("\t\tAika: _______________\tPaikka: _______________", styles.text),
+                p("Palautus", styles.h2),
+                p("\t\tAika: _______________\tPaikka: _______________", styles.text),
+                
+                pdf.SignatureBlock(("Vuokranantaja", "Vuokraaja"), ("%(column)s", "Nimen selvennys", "Aika ja paikka"), {
+                    ('Vuokranantaja', 'Nimen selvennys'): "Joel Pirttimaa",
+                    ('Vuokranantaja', 'Aika ja paikka'): 'Otaniemi, %(today)s',
+                    ('Vuokraaja', 'Aika ja paikka'): 'Otaniemi, %(today)s',
+                }),
+        ]
+       
+        # render elements to buf as PDF code
+        pdf_code = doc.render_string(elements)
+
+        return Response(pdf_code, mimetype='application/pdf')
+
+class Index (PageHandler) :
+    DATA = (
+        (100, "Top A", []),
+        (200, "Top B", [
+            (210, "Mid BA", [
+                (211, "Sub BAA", []),
+                (212, "Sub BAB", []),
+            ]),
+            (220, "Mid BB", []),
+        ]),
+        (300, "Top C", [
+            (310, "Mid CA", []),
+            (320, "Mid CB", []),
+        ]),
+    )
+
+    def render (self) :
+        # build data set
+        data = self.DATA
+
+        def render_item (id, text, children) :
+            return tags.li(
+                tags.div(class_='item')(
+                    # item text
+                    tags.a(tags.input(type='checkbox', name_=id, checked=(
+                        'checked' if self.request.form.get(str(id)) else None
+                    )), text, class_='text'),
+                ),
+
+                # children
+                tags.ul(render_item(*item) for item in children if item) if children else None,
+
+                # 
+                class_=('more open' if children else None),
+            )
+
+        # render it
+        return (
+            tags.h3("Item list"),
+            tags.form(action='.', method='post')(
+                tags.ul(class_='treelist')(render_item(*item) for item in data),
+                tags.input(type='submit'),
+            ),
+        )
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svv/customers.py	Wed Dec 22 02:52:25 2010 +0200
@@ -0,0 +1,70 @@
+# coding: utf-8
+"""
+    Customer/contact data model/view/handler
+"""
+
+from svv.controllers import PageHandler
+from svv.html import tags
+from svv import database as db
+import svv.urls
+
+class CustomersView (PageHandler) :
+    """
+        Page for main 'customers' view
+    """
+
+    def render (self) :
+        # database
+        conn = self.app.get_connection()
+
+        # customers
+        customers = list(conn.execute(db.select([db.customers])))
+        contacts = list(conn.execute(db.contacts.join(db.customers).select(use_labels=True)))
+        #[
+        #    db.contacts.c.id, db.customers.c.id, db.customers.c.name, db.contacts.c.name,
+        #    db.contacts.c.phone, db.contacts.c.email,
+        #]
+
+        return (
+            tags.h1("Asiakkaat"),
+            tags.p(u"Tunnen %d asiakasta ja %d yhteyshenkilöä." % (len(customers), len(contacts))),
+
+            tags.table(
+                tags.tr(
+                    tags.th("ID #"),
+                    tags.th("Nimi"),
+                ),
+                (tags.tr(
+                    tags.td(tags.a(href=self.build_url(CustomerView, id=customer[db.customers.c.id]))('#%d' % customer[db.customers.c.id])),
+                    tags.td(tags.a(href=self.build_url(CustomerView, id=customer[db.customers.c.id]))(customer[db.customers.c.name])),
+                ) for customer in customers)
+            ),
+
+            tags.table(
+                tags.tr(
+                    tags.th("ID #"),
+                    tags.th("Asiakas"),
+                    tags.th("Nimi"),
+                    tags.th("Puhelinnumero"),
+                    tags.th(u"Sähköpostiosoite"),
+                ),
+                (tags.tr(
+                    tags.td(tags.a(href='#')('#%d' % row[db.contacts.c.id])),
+                    tags.td(
+                        tags.a(href=self.build_url(CustomerView, id=row[db.customers.c.id]))(row[db.customers.c.name])
+                            if row[db.customers.c.id] else " "
+                    ),
+                    tags.td(
+                        tags.a(href='#')(row[db.contacts.c.name]),
+                    ),
+                    tags.td(row[db.contacts.c.phone]),
+                    tags.td(row[db.contacts.c.email]),
+                ) for row in contacts)
+            ),
+        )
+
+class CustomerView (PageHandler) :
+    def render (self, id) :
+        return tags.h1("Info for customer #%d" % id)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svv/database.py	Wed Dec 22 02:52:25 2010 +0200
@@ -0,0 +1,89 @@
+# coding: utf-8
+
+"""
+    Database schema
+"""
+
+from sqlalchemy import *
+
+# our schema
+schema = MetaData()
+
+#
+# tables
+#
+
+# customers are organizations or natural persons who are serving as the renting side of an order
+customers = Table('customers', schema,
+        Column('id', Integer, primary_key=True),
+
+        # organization or person name (may match with contact name)
+        Column('name', Unicode, nullable=False),
+
+        # free-form internal notes
+        Column('notes', Unicode),
+
+        # raw CREATE/UPDATE timestamp
+        Column('created_at', DateTime, nullable=False, default=func.now()),
+        Column('updated_at', DateTime, nullable=True, onupdate=func.current_timestamp()),
+)
+
+# contacts are phone/email contact details for named persons in organizations who are active in some order
+contacts = Table('contacts', schema,
+        Column('id', Integer, primary_key=True),
+
+        # reference to contact being primary for specified customer (organization or themselves)
+        Column('customer', None, ForeignKey('customers.id'), nullable=True),
+
+        # person's natural name
+        Column('name', Unicode, nullable=True),
+
+        # freeform telephone number
+        Column('phone', String, nullable=True),
+
+        # freeform email address
+        Column('email', String, nullable=True),
+
+        # raw CREATE/UPDATE timestamp
+        Column('created_at', DateTime, nullable=False, default=func.now()),
+        Column('updated_at', DateTime, nullable=True, onupdate=func.current_timestamp()),
+)
+
+# orders are the transactions that some customer/contact makes, and involve checking out some inventory for some
+# duration
+orders = Table('orders', schema,
+        Column('id', Integer, primary_key=True),
+
+        # ordering customer, if entered (i.e. who to bill)
+        Column('customer', None, ForeignKey('customers.id'), nullable=True),
+
+        # active contact at customer side for order (i.e. who to call)
+        Column('contact', None, ForeignKey('contacts.id'), nullable=True),
+
+        # short description of customer event where these items are going
+        Column('event_name', Unicode, nullable=True),
+
+        # longer description of what the event is
+        Column('event_description', Unicode, nullable=True),
+        
+        # rough duration of event; time interval during which items are reserved
+        Column('start', DateTime),
+        Column('end', DateTime),
+        
+        # raw CREATE/UPDATE timestamp
+        Column('created_at', DateTime, nullable=False, default=func.now()),
+        Column('updated_at', DateTime, nullable=True, onupdate=func.current_timestamp()),
+)
+
+# editable contract terms for customer order
+# provision for standard terms, or more specific ones for certain customers
+contract_terms = Table('contract_terms', schema,
+        Column('id', Integer, primary_key=True),
+
+        # short description
+        Column('name', Unicode, nullable=False),
+
+        # full terms in formatted code
+        Column('terms', Unicode, nullable=False),
+)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svv/orders.py	Wed Dec 22 02:52:25 2010 +0200
@@ -0,0 +1,87 @@
+# coding: utf-8
+"""
+    Order data model/view/handler
+"""
+
+from svv.controllers import PageHandler
+from svv.html import tags
+from svv import database as db
+
+class OrdersView (PageHandler) :
+    def render (self) :
+        return tags.h1("Orders list")
+
+class OrderView (PageHandler) :
+    def render (self) :
+        return tags.h1("Order info")
+
+class NewOrderView (PageHandler) :
+    def render_form_field (self, title, description, name, input) :
+        return tags.li(class_='field')(
+            tags.label(title, for_=name),
+
+            input,
+
+            # tags.span("Error!"),
+
+            tags.p(description),
+        )
+
+    def render (self) :
+        return tags.form(action='.', method='POST')(
+            tags.h1(u"Uusi tilaus"),
+
+            tags.fieldset(
+                tags.legend(u"Tilaaja"),
+                
+                tags.ol(
+                    self.render_form_field(u"Tilaaja", u"Tilaavan yhdistyksen/henkilön nimi", 'customer_name', (
+                        tags.select(name='customer_id', id='customer_id')(
+                            tags.option(value=0, selected='selected')(u"Luo uusi"),
+                            tags.option(value=1)(u"Esimerkkiasiakas #1"),
+                            tags.option(value=2)(u"Esimerkkiasiakas #2"),
+                        ),
+                        tags.input(type='text', name='customer_name', id='customer_name'),
+                        tags.script(r"$(document).ready(function () { $('#customer_id').formSelectPreset({textTarget: $('#customer_name')}); });"),
+                    )),
+
+                    self.render_form_field(u"Yhteyshenkilö", u"Yhteyshenkilön nimi, jos eri kun tilaaja", 'contact_name', (
+                        tags.select(name='contact_id')(
+                            tags.option(value=0, selected='selected')(u"Luo uusi"),
+                            tags.option(value=1)(u"Esimerkkihenkilö #1"),
+                            tags.option(value=2)(u"Esimerkkihenkilö #2"),
+                        ),
+                        tags.input(type='text', name='contact_name')
+                    )),
+
+                    self.render_form_field(u"Puhelin", u"Yhteyshenkilön puhelinnumero", 'contact_phone', (
+                        tags.input(type='text', name='contact_phone')
+                    )),
+
+                    self.render_form_field(u"Sähköposti", u"Yhteyshenkilön sähköpostiosoite", 'contact_email', (
+                        tags.input(type='text')
+                    )),
+                ),
+            ),
+
+            tags.fieldset(
+                tags.legend(u"Tapahtuma"),
+           
+                tags.ol(
+                    self.render_form_field(u"Tapahtuma", u"Tapahtuman lyhyt nimi", 'event_name', (
+                        tags.input(type='text', name='event_name')
+                    )),
+
+                    self.render_form_field(u"Lisätiedot", u"Tapahtuman tarkemmat tiedot", 'event_description', (
+                        tags.textarea("", rows=8, name='event_description')
+                    )),
+
+                    self.render_form_field(u"Ajankohta", u"Tapahtuman ajankohta (kamat noudetaan - palautetaan)", 'event_start', (
+                        tags.input(type='text', name='event_start'),
+                        " - ",
+                        tags.input(type='text', name='event_end'),
+                    )),
+                ),
+            ),
+        )
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svv/urls.py	Wed Dec 22 02:52:25 2010 +0200
@@ -0,0 +1,26 @@
+"""
+    App URL maps 
+"""
+
+from werkzeug.routing import Map, Rule
+
+# controllers
+from svv.controllers import Index, Document
+from svv.customers import CustomersView, CustomerView
+from svv.orders import OrdersView, OrderView, NewOrderView
+
+# map URLs -> AppHandler
+URLS = Map((
+    Rule('/orders/', endpoint=OrdersView),
+    Rule('/orders/<int:id>', endpoint=OrderView),
+    Rule('/orders/new', endpoint=NewOrderView),
+
+    Rule('/customers', endpoint=CustomersView),
+    Rule('/customers/<int:id>', endpoint=CustomerView),
+
+    # test stuff
+    Rule('/', endpoint=Index),
+    Rule('/pdf/<string:name>.pdf', endpoint=Document),
+))
+
+
--- a/svv/wsgi.py	Wed Dec 22 02:51:41 2010 +0200
+++ b/svv/wsgi.py	Wed Dec 22 02:52:25 2010 +0200
@@ -4,237 +4,31 @@
     WSGI frontend/entry point
 """
 
-from svv import pdf
-
 import werkzeug
 from werkzeug import exceptions
 from werkzeug import Request, Response
-from werkzeug.routing import Map, Rule
 
-import logging
-            
+import logging            
 import datetime
 
+from svv import pdf
+from svv import database as db
+from svv.urls import URLS
+
 # logging
 log = logging.getLogger('svv.wsgi')
 
-class AppHandler (object):
-    """
-        Per-request handler type, containing the request context and implementing the response.
-
-        Works on a generic level, i.e. WSGI/Werkzeug URL-mapped request in, response out.
-    """
-
-    # default content type for response
-    CONTENT_TYPE = 'text/html'
-
-    def __init__ (self, request) :
-        """
-            Initialize for processing the given Request, to prepare for action-method later on
-        """
-
-        self.request = request
-
-    def respond (self, url_values) :
-        """
-            Handle request that was mapped to ourselves via the URL routing, using given dict of values from URL.
-        """
-
-        raise NotImplementedError()
-
-import html
-from html import tags
-
-class PageHandler (AppHandler) :
-    """
-        Specialized AppHandler for normal HTML page views.
-
-        Renders the layout template and HTML.
-    """
-
-    def respond (self, url_values) :
-        title = "Index"
-        css = ["/static/layout.css", "/static/style.css", "/static/treelist.css"]
-        scripts = [
-                "/static/js/prototype.js", 
-                "/static/js/scriptaculous/scriptaculous.js", 
-                "/static/js/treelist.js"
-        ]
-
-        head = (
-            tags.title(title),
-            (tags.link(rel='Stylesheet', type="text/css", href=src) for src in css),
-            # XXX: script can't self-close >_<
-            (tags.script(src=src)(" ") for src in scripts),
-        )
-
-        header = ("Foo List")
-        nav = [tags.ul(tags.li(tags.a(href='#')(name)) for name in ['Nav A', 'Nav B', 'Nav C'])]
-        menu = [tags.ul(tags.li(tags.a(href='#')(name)) for name in ['Menu A', 'Menu B', 'Menu C'])]
-        footer = ("Copyright?")
-
-        # render page HTML
-        content = self.render(**url_values)
-        
-        layout = (
-            tags.div(id='header')(header),
-            tags.div(id='container')(
-                tags.div(id='menu')(menu),
-                tags.div(id='nav')(nav),
-                tags.div(id='content')(content),
-            ),
-            tags.div(id='footer')(footer),
-        )
-        
-        # perform the actual rendering (run generators etc.)
-        html_text = unicode(html.document(head, layout))
-        
-        # response object
-        # XXX: unicode?
-        return Response(html_text, mimetype='text/html')
-
-    def render (self, **args) :
-        """
-            Render and return HTML for page content.
-
-            XXX: must be HTML object, support just text?
-        """
-
-        raise NotImplementedError()
-
-class Document (AppHandler) :
-    """
-        PDF generation/export
-    """
-
-    def respond (self, url_values) :
-
-        title = url_values.get('name', "Hello World")
-
-        tpl = pdf.PageTemplate('id', 
-            header_columns  = (
-                ("", ""),
-                ("", ""),
-                ("", ""),
-                ("Vuokrasopimus", "%(today)s\n" + title + "\n"),
-            ),
-            footer_columns  = (
-                ("Teekkarispeksi Ry", "www.teekkarispeksi.fi"),
-                ("Tekniikkavastaava", "Juha Kallas\n045 xxx yyzz\njskallas@cc.hut.fi"),
-                ("Varastovastaava", "Joel Pirttimaa\n045 xxx yyzz\njhpirtti@cc.hut.fi"),
-                ("", ""),
-            ),
-        )
-
-        doc = pdf.DocumentTemplate([tpl],
-            title = title, author = "A. N. Onous"
-        )
-
-        # stylesheet
-        styles = pdf.Styles
-
-        # tree root
-        list_seq = pdf.ListItem.seq
-        tree = pdf.ListItem("Sopimusehdot", styles.h2, None, list_seq(), [
-            pdf.ListItem("Osapuolet", styles.list_h2, None, list_seq(), [
-                pdf.ListItem(None, None, "Teekkarispeksi ry (Y-tunnus 1888541-7), jäljempänä “Vuokranantaja”."),
-                pdf.ListItem(None, None, title + u", jäljempänä “Vuokraaja”. 1.1 ja 1.2 jäljempänä yhdessä “osapuolet”.")
-            ]),
-            pdf.ListItem("Kaluston lainaaminen", styles.list_h2, None, list_seq(), [
-                pdf.ListItem("Yleistä", styles.list_h3, "Tässä sopimuksessa sovitaan toiminnasta Vuokranantajan lainatessa tanssimattoja Vuokraajalle"),
-                pdf.ListItem("Vuokranantajan velvollisuudet", styles.list_h3, "Vuokranantaja sitoutuu toimittamaan sovittuna ajankohtana Vuokraajalle erikseen sovittava (liite) määrä tanssimattoja."),
-                pdf.ListItem("Blaa Blaa", styles.list_h3, "Etc."),
-            ]),
-            pdf.ListItem("Tätä sopimusta ja sitä koskevia erimielisyyksiä koskevat määräykset", styles.list_h2, None, list_seq(), [
-                pdf.ListItem("Sopimuksen voimassaolo", styles.list_h3, "Sopimus on osapuolia sitova sen jälkeen, kun osapuolet ovat sen allekirjoittaneet."),
-                pdf.ListItem("Muutosten tekeminen", styles.list_h3, "Muutokset sopimukseen on tehtävä kirjallisesti molempien osapuolten kesken."),
-                pdf.ListItem("Blaa Blaa", styles.list_h3, "Etc."),
-            ]),
-        ])
-
-        from reportlab.platypus import Paragraph as p
-
-        elements = [
-                p("Vuokrasopimus", styles.h1),
-                p("Teekkarispeksi ry AV-tekniikka", styles.h3),
-        ] + list(tree.render_pdf()) + [
-                p("Nouto", styles.h2),
-                p("\t\tAika: _______________\tPaikka: _______________", styles.text),
-                p("Palautus", styles.h2),
-                p("\t\tAika: _______________\tPaikka: _______________", styles.text),
-                
-                pdf.SignatureBlock(("Vuokranantaja", "Vuokraaja"), ("%(column)s", "Nimen selvennys", "Aika ja paikka"), {
-                    ('Vuokranantaja', 'Nimen selvennys'): "Joel Pirttimaa",
-                    ('Vuokranantaja', 'Aika ja paikka'): 'Otaniemi, %(today)s',
-                    ('Vuokraaja', 'Aika ja paikka'): 'Otaniemi, %(today)s',
-                }),
-        ]
-       
-        # render elements to buf as PDF code
-        pdf_code = doc.render_string(elements)
-
-        return Response(pdf_code, mimetype='application/pdf')
-
-class Index (PageHandler) :
-    DATA = (
-        (100, "Top A", []),
-        (200, "Top B", [
-            (210, "Mid BA", [
-                (211, "Sub BAA", []),
-                (212, "Sub BAB", []),
-            ]),
-            (220, "Mid BB", []),
-        ]),
-        (300, "Top C", [
-            (310, "Mid CA", []),
-            (320, "Mid CB", []),
-        ]),
-    )
-
-    def render (self) :
-        # build data set
-        data = self.DATA
-
-        def render_item (id, text, children) :
-            return tags.li(
-                tags.div(class_='item')(
-                    # item text
-                    tags.a(tags.input(type='checkbox', name_=id, checked=(
-                        'checked' if self.request.form.get(str(id)) else None
-                    )), text, class_='text'),
-                ),
-
-                # children
-                tags.ul(render_item(*item) for item in children if item) if children else None,
-
-                # 
-                class_=('more open' if children else None),
-            )
-
-        # render it
-        return (
-            tags.h3("Item list"),
-            tags.form(action='.', method='post')(
-                tags.ul(class_='treelist')(render_item(*item) for item in data),
-                tags.input(type='submit'),
-            ),
-        )
-
-
 class WSGIApp (object) :
     """
         Top-level WSGI handler impl
     """
 
-    # URL paths
-    # map to AppHandler-endpoint
-    URLS = Map((
-        Rule('/', endpoint=Index),
-        Rule('/pdf/<string:name>.pdf', endpoint=Document),
-    ))
-
-    def __init__ (self) :
-        pass
+    def __init__ (self, app) :
+        """
+            app     - the top-level Application state used by handlers
+        """
+        
+        self.app = app
 
     # wrap to use werkzeug's Request/Response
     @Request.application
@@ -264,13 +58,13 @@
         """
 
         # map URLs against this request
-        urls = self.URLS.bind_to_environ(req)
+        urls = URLS.bind_to_environ(req)
 
         # lookup matching URL for handler type and matched values from URL
         url_handler, url_values = urls.match()
 
         # the per-request handler (from endpoint)
-        req_handler = url_handler(req)
+        req_handler = url_handler(self.app, req, urls)
 
         # XXX: per-method thing?
         response = req_handler.respond(url_values)