--- 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)