Optionally take contract text from form input
authorTero Marttila <terom@fixme.fi>
Fri, 07 Jan 2011 02:06:49 +0200
changeset 32 10c48a6843ad
parent 31 e1b63e4d10f4
child 33 471837eb3d96
Optionally take contract text from form input
static/forms.css
svv/controllers.py
svv/orders.py
svv/wsgi.py
--- a/static/forms.css	Fri Jan 07 01:23:24 2011 +0200
+++ b/static/forms.css	Fri Jan 07 02:06:49 2011 +0200
@@ -94,6 +94,12 @@
     padding: 4px;
 }
 
+/* A multi-line text edit widget is wide enough for lots of text */
+form textarea
+{
+    width: 80%;
+}
+
 /* A field that failed validation is highlighted */
 form .failed input,
 form .failed textarea
--- a/svv/controllers.py	Fri Jan 07 01:23:24 2011 +0200
+++ b/svv/controllers.py	Fri Jan 07 02:06:49 2011 +0200
@@ -54,12 +54,27 @@
         # MultiDict from werkzeug.Request
         return self.request.form
 
-    def respond (self, url_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()
+        # process e.g. POST data for e.g. redirect
+        response = self.process(**url_values)
+        
+        if not response :
+            # assume superclass does something else if process didn't handle it
+            pass
+
+        return response
+
+    def process (self, **args) :
+        """
+            Process incoming POST data, optionally returning a redirect response.
+        """
+        
+        # default to ignore
+        pass
 
 class PageHandler (AppHandler) :
     """
@@ -155,7 +170,7 @@
         # ok
         return response
 
-    def respond (self, url_values) :
+    def respond (self, **url_values) :
         """
             Build and return a response from the following steps:
 
@@ -163,8 +178,8 @@
             * render() -> render_content() as HTML
         """
 
-        # process e.g. POST data for e.g. redirect
-        response = self.process(**url_values)
+        # optional processing
+        response = super(PageHandler, self).respond(**url_values)
 
         if not response :
             # render page HTML
@@ -177,35 +192,31 @@
         # ok
         return response
 
-    def process (self, **args) :
-        """
-            Process incoming POST data, optionally returning a redirect response.
-        """
-        
-        # default to ignore
-        pass
-
 class DocumentHandler (AppHandler) :
     """
         PDF generation/export
     """
 
-    def respond (self, url_values) :
+    def respond (self, **url_values) :
         """
             Generate the document, and return it as a .pdf file, with the filename generated from the document's title.
         """
-
-        # XXX: proper support
-        self.process(**url_values)
         
-        pdf_file = self.generate(**url_values)
+        # optional processing
+        response = super(DocumentHandler, self).respond(**url_values)
+        
+        if not response :
+            pdf_file = self.generate(**url_values)
 
-        # file wrapper
-        # XXX: is this any use at all for StringIO?
-        pdf_file = werkzeug.wrap_file(self.request.environ, pdf_file)
+            # file wrapper
+            # XXX: is this any use at all for StringIO?
+            pdf_file = werkzeug.wrap_file(self.request.environ, pdf_file)
 
-        # respond with file wrapper
-        return Response(pdf_file, mimetype='application/pdf', direct_passthrough=True)
+            # respond with file wrapper
+            response = Response(pdf_file, mimetype='application/pdf', direct_passthrough=True)
+        
+        # ok
+        return response
 
     def generate (self, **url_values) :
         """
--- a/svv/orders.py	Fri Jan 07 01:23:24 2011 +0200
+++ b/svv/orders.py	Fri Jan 07 02:06:49 2011 +0200
@@ -163,6 +163,14 @@
 
         self.errors[field].append(form_error)
 
+    def build_name_for_armed_input (self, name) :
+        """
+            Return the name used for the checkbox associated with the named armed text input
+        """
+        
+        return name + '_enabled'
+
+
     def process_raw_field (self, name, default=None, required=None) :
         """
             Process a generic incoming data field.
@@ -182,7 +190,7 @@
         else :
             return default
 
-    def process_string_field (self, name, default=None, required=None, strip=True) :
+    def process_text_field (self, name, default=None, required=None, strip=True) :
         """
             Process a generic incoming string field.
 
@@ -246,6 +254,44 @@
         except ValueError, ex :
             raise FormError(name, value, "Invalid date/time value: " + str(ex))
 
+    def process_checkbox_field (self, name, required=None) :
+        """
+            Process an incoming checkbox input's value.
+            
+            Any non-empty/non-whitespace value will be accepted as True.
+        """
+
+        value = self.process_raw_field(name, required)
+
+        if not value and required :
+            raise FormError(name, value, "Must be checked")
+
+        elif value and value.strip() :
+            # checked
+            return True
+
+        else :
+            # unchecked
+            return False
+        
+    def process_armed_text_field (self, name, required=None) :
+        """
+            A text field that must be enabled by a checkbox before being used.
+
+            If not enabled, returns False. Otherwise, text field value, and fails if empty but required.
+        """
+
+        checkbox_name = self.build_name_for_armed_input(name)
+        
+        if self.process_checkbox_field(checkbox_name) :
+            # use input value
+            return self.process_text_field(name, required=required)
+
+        else :
+            # not selected
+            return False
+
+
     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.
@@ -323,18 +369,23 @@
     
         return not self.errors
 
-    def render_text_input (self, name, value=None, multiline=False) :
+    def render_text_input (self, name, value=None, multiline=False, rows=10, autoscale=True) :
         """
             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
+                rows            - number of rows for <textarea> per default
+                autoscale       - automatically scale the <textarea> to the number of lines of text
         """
 
         if multiline :
+            if autoscale and value :
+                rows = value.count('\n') * 5 / 4
+
             # XXX: textarea can't be self-closing for some reason?
-            return tags.textarea(name=name, id=name, _selfclosing=False, _whitespace_sensitive=True)(value)
+            return tags.textarea(name=name, id=name, rows=rows, _selfclosing=False, _whitespace_sensitive=True)(value)
 
         else :
             return tags.input(type='text', name=name, id=name, value=value)
@@ -368,6 +419,42 @@
             tags.script("$(document).ready(function () { $('#" + name + "').datetimepicker(); });"),
         )
 
+    def render_checkbox_input (self, name, checked=None) :
+        """
+            Render HTML for a checkbox.
+        """
+
+        return tags.input(type="checkbox", name=name, id=name, value="1",
+            checked=("checked" if checked else None)
+        )
+
+    def render_armed_text_input (self, name, value=False, default=None) :
+        """
+            Render HTML for a text field that must be enabled by a checkbox before being used.
+
+                value       - the three-state value
+                                False       - not checked, use default as value
+                                None        - checked, use empty value
+                                str         - checked, ues value
+        """
+
+        checkbox_name = self.build_name_for_armed_input(name)
+
+        if value is False :
+            checked = False
+            value = default
+        
+        else :
+            checked = True
+
+        return (
+            self.render_checkbox_input(checkbox_name, checked),
+            self.render_text_input(name, value),
+
+            tags.script("$(document).ready(function () { $('#" + name + "').formEnabledBy($('#" + checkbox_name + "')); });")
+        )
+
+
     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>.
@@ -439,7 +526,7 @@
 
         try :
             customer_id = self.process_integer_field('customer_id')
-            customer_name = self.process_string_field('customer_name')
+            customer_name = self.process_text_field('customer_name')
 
             if not customer_id and not customer_name :
                 raise FormError('customer_name', None, "Must enter a customer")
@@ -465,9 +552,9 @@
         
         try :
             contact_id = self.process_integer_field('contact_id')
-            contact_name = self.process_string_field('contact_name')
-            contact_phone = self.process_string_field('contact_phone')
-            contact_email = self.process_string_field('contact_email')
+            contact_name = self.process_text_field('contact_name')
+            contact_phone = self.process_text_field('contact_phone')
+            contact_email = self.process_text_field('contact_email')
             contact_customer = customer_id
 
             if not contact_id and not contact_name and (contact_phone or contact_email) and customer_name :
@@ -500,8 +587,8 @@
         """
         
         try :
-            event_name = self.process_string_field('event_name')
-            event_description = self.process_string_field('event_description', strip=False)
+            event_name = self.process_text_field('event_name')
+            event_description = self.process_text_field('event_description', strip=False)
             event_start = self.process_datetime_field('event_start')
             event_end = self.process_datetime_field('event_end')
 
@@ -741,6 +828,47 @@
 
     DEFAULT_PLACE = u"Otaniemi"
 
+    DEFAULT_TEXT = u"""\
+# Vuokrasopimus
+### Teekkarispeksi ry AV-tekniikka
+
+## Sopimusehdot
+
+1.  ## Osapuolet
+    1.  Teekkarispeksi ry (Y-tunnus 1888541-7), jäljempänä “Vuokranantaja”.
+    2.  {order.customer.name}, jäljempänä “Vuokraaja”. 1.1 ja 1.2 jäljempänä yhdessä “osapuolet”.
+
+2.  ## Kaluston lainaaminen
+    1.  ### Yleistä
+        Tässä sopimuksessa sovitaan toiminnasta Vuokranantajan lainatessa tanssimattoja Vuokraajalle.
+
+    2.  ### Vuokranantajan velvollisuudet
+        Vuokranantaja sitoutuu toimittamaan sovittuna ajankohtana Vuokraajalle erikseen sovittava (liite) määrä tanssimattoja.
+
+    3.  ### Blaa Blaa
+        Etc.
+
+3. ## Tätä sopimusta ja sitä koskevia erimielisyyksiä koskevat määräykset
+    1.  ### Sopimuksen voimassaolo
+        Sopimus on osapuolia sitova sen jälkeen, kun osapuolet ovat sen allekirjoittaneet.
+
+    2.  ### Muutosten tekeminen
+        Muutokset sopimukseen on tehtävä kirjallisesti molempien osapuolten kesken.
+
+    3.  ### Blaa Blaa
+
+        Etc.
+
+## Nouto
+    Aika: _______________       Paikka: _______________
+
+## Palautus
+    Aika: _______________       Paikka: _______________
+        """
+
+    # user clicked 'Edit' button, i.e. they want to view the rest of the edit fields
+    edit = None
+
     def defaults (self) :
 
         today = datetime.date.today()
@@ -749,89 +877,10 @@
         self.prefill_placetime_default = "%s, %s" % (self.DEFAULT_PLACE, today.strftime("%d.%m.%Y"))
         self.prefill_ourname = False
         self.prefill_ourname_default = None
-
-    def build_name_for_armed_input (self, name) :
-        """
-            Return the name used for the checkbox associated with the named armed text input
-        """
         
-        return name + '_enabled'
-
-    def render_checkbox_input (self, name, checked=None) :
-        """
-            Render HTML for a checkbox.
-        """
-
-        return tags.input(type="checkbox", name=name, id=name, value="1",
-            checked=("checked" if checked else None)
-        )
-
-    def render_armed_text_input (self, name, value=False, default=None) :
-        """
-            Render HTML for a text field that must be enabled by a checkbox before being used.
-
-                value       - the three-state value
-                                False       - not checked, use default as value
-                                None        - checked, use empty value
-                                str         - checked, ues value
-        """
-
-        checkbox_name = self.build_name_for_armed_input(name)
-
-        if value is False :
-            checked = False
-            value = default
-        
-        else :
-            checked = True
-
-        return (
-            self.render_checkbox_input(checkbox_name, checked),
-            self.render_text_input(name, value),
+        self.edit = False
 
-            tags.script("$(document).ready(function () { $('#" + name + "').formEnabledBy($('#" + checkbox_name + "')); });")
-        )
-
-    def process_checkbox_field (self, name, required=None) :
-        """
-            Process an incoming checkbox input's value.
-
-            The value must be a literal "1" to be accepted as True, and only a missing or empty value will be
-            recognized as False.
-        """
-
-        value = self.process_raw_field(name, required)
-
-        if not value and required :
-            raise FormError(name, value, "Must be checked")
-
-        elif not value :
-            # unchecked
-            return False
-
-        elif value == "1" :
-            # checked
-            return True
-        
-        else :
-            raise FormError(name, value, "Unrecognized checkbox state")
-
-    def process_armed_text_field (self, name, required=None) :
-        """
-            A text field that must be enabled by a checkbox before being used.
-
-            If not enabled, returns False. Otherwise, text field value, and fails if empty but required.
-        """
-
-        checkbox_name = self.build_name_for_armed_input(name)
-        
-        if self.process_checkbox_field(checkbox_name) :
-            # use input value
-            return self.process_string_field(name, required=required)
-
-        else :
-            # not selected
-            return False
+        self.contract_text = self.DEFAULT_TEXT
 
     def render (self, action, submit=u"Tulosta Vuokrasopimus") :
         
@@ -849,8 +898,16 @@
                         self.render_armed_text_input('prefill_ourname', self.prefill_ourname, self.prefill_ourname_default)
                     )),
 
+                    (
+                        self.render_form_field('contract_text', u"Sopimusteksti", u"Vuokrasopimuksen vapaamuotoinen otsikko, teksti, sopimusehdot, yms. Voidaan käyttää muuttujakenttiä (i.e. {...}). XXX: listaa kentät", (
+                            self.render_text_input('contract_text', self.contract_text, multiline=True, autoscale=True)
+                        )),
+
+                    ) if self.edit else None,
+
                     tags.li(
                         tags.input(type='submit', value=submit),
+                        tags.input(type='submit', name='edit', value=u"Muokkaa lisää"),
                     ),
                 ),
             ),
@@ -859,13 +916,23 @@
 
 
     def process (self, data) :
+        """
+            Bind incoming data.
+
+            Returns true if no error input, and not requesting the full editing form ("Edit more" button).
+        """
+
         # bind the raw post data
         self.data = data
 
         self.prefill_placetime = self.process_armed_text_field('prefill_placetime')
         self.prefill_ourname = self.process_armed_text_field('prefill_ourname')
+
+        self.edit = self.process_checkbox_field('edit')
         
-        return not self.errors
+        self.contract_text = self.process_text_field('contract_text', default=self.DEFAULT_TEXT)
+        
+        return not self.errors and not self.edit
 
 class OrdersView (PageHandler) :
     """
@@ -1205,21 +1272,6 @@
         Generate and return PDF document for rental contract.
     """
 
-    def load_params (self, data) :
-        """
-            Return OrderContractForm with parameters for generation
-        """
-
-        form = OrderContractForm(self.app)
-
-        form.defaults()
-
-        if data :
-            # XXX: can't fail
-            form.process(data)
-
-        return form
-
     def process (self, id):
         """
             Return OrderModel object for given ID
@@ -1231,8 +1283,40 @@
         # order object
         self.order = self.session.query(Order).options(db.eagerload(Order.customer), db.eagerload(Order.contact)).get(id)
 
-        # form params
-        self.params = self.load_params(self.POST)
+        # form
+        self.form = OrderContractForm(self.app)
+        self.form.defaults()
+        
+        # XXX: also use GET data?
+        data = self.request.values
+
+        if self.form.process(data) :
+            # render PDF
+            pass
+
+        else :
+            # show form again
+
+            # XXX: fix the pagelayout shit, this is brutal
+            handler = PageHandler(self.app, self.request, self.urlmap)
+
+            # monkey-map
+            handler.render_content = self.render_content
+
+            log.warn("Monkeyhack %r -> %r", self, handler)
+            
+            # XXX: this invokes handler.process(), but...
+            return handler.respond(id=id)
+    
+    def render_content (self, id) :
+        """
+            Display PDF gen form.
+        """
+        
+        return (
+            tags.h1(u"Vuokrasopimus"),
+            self.form.render(action=self.url_for(OrderContractDocument, id=id)),
+        )
     
     def generate_document (self, id) :
         """
@@ -1240,7 +1324,7 @@
         """
 
         order = self.order
-        params = self.params
+        params = self.form
 
         title = "Teekkarispeksi Ry - Vuokrasopimus"
         author = "Teekkarispeksi Ry"
@@ -1270,47 +1354,8 @@
         from reportlab.platypus import Paragraph as p
         
 
-        text = u"""\
-# Vuokrasopimus
-### Teekkarispeksi ry AV-tekniikka
-
-## Sopimusehdot
-
-1.  ## Osapuolet
-    1.  Teekkarispeksi ry (Y-tunnus 1888541-7), jäljempänä “Vuokranantaja”.
-    2.  {order.customer.name}, jäljempänä “Vuokraaja”. 1.1 ja 1.2 jäljempänä yhdessä “osapuolet”.
-
-2.  ## Kaluston lainaaminen
-    1.  ### Yleistä
-        Tässä sopimuksessa sovitaan toiminnasta Vuokranantajan lainatessa tanssimattoja Vuokraajalle.
-
-    2.  ### Vuokranantajan velvollisuudet
-        Vuokranantaja sitoutuu toimittamaan sovittuna ajankohtana Vuokraajalle erikseen sovittava (liite) määrä tanssimattoja.
-
-    3.  ### Blaa Blaa
-        Etc.
-
-3. ## Tätä sopimusta ja sitä koskevia erimielisyyksiä koskevat määräykset
-    1.  ### Sopimuksen voimassaolo
-        Sopimus on osapuolia sitova sen jälkeen, kun osapuolet ovat sen allekirjoittaneet.
-
-    2.  ### Muutosten tekeminen
-        Muutokset sopimukseen on tehtävä kirjallisesti molempien osapuolten kesken.
-
-    3.  ### Blaa Blaa
-
-        Etc.
-
-## Nouto
-    Aika: _______________       Paikka: _______________
-
-## Palautus
-    Aika: _______________       Paikka: _______________
-
-        """
-
         # format
-        text = text.format(
+        text = params.contract_text.format(
             order       = order,
         )
         
--- a/svv/wsgi.py	Fri Jan 07 01:23:24 2011 +0200
+++ b/svv/wsgi.py	Fri Jan 07 02:06:49 2011 +0200
@@ -67,7 +67,7 @@
         req_handler = url_handler(self.app, req, urls)
 
         # XXX: per-method thing?
-        response = req_handler.respond(url_values)
+        response = req_handler.respond(**url_values)
 
         # ok
         return response