svv/orders.py
changeset 11 90a3c570c227
parent 10 4bdb45071c89
child 12 2d3fb967cd30
equal deleted inserted replaced
10:4bdb45071c89 11:90a3c570c227
     7 from svv.html import tags
     7 from svv.html import tags
     8 from svv import database as db
     8 from svv import database as db
     9 
     9 
    10 import datetime
    10 import datetime
    11 import logging
    11 import logging
       
    12 import collections
    12 
    13 
    13 log = logging.getLogger('svv.orders')
    14 log = logging.getLogger('svv.orders')
    14 
    15 
    15 class FormError (Exception) :
    16 class FormError (Exception) :
    16     """
    17     """
    17         A user-level error in a form field
    18         A user-level error in a form field
    18     """
    19     """
    19 
    20 
    20     def __init__ (self, field, error) :
    21     def __init__ (self, field, value, error) :
    21         """
    22         """
    22                 field       - name of field with error
    23                 field       - name of field with error
       
    24                 value       - the errenous value in the form that we recieved it
       
    25                               may be None if it was the *lack* of a value that caused the issue
    23                 error       - descriptive text for user
    26                 error       - descriptive text for user
    24         """
    27         """
    25 
    28 
    26         self.field = field
    29         self.field = field
       
    30         self.value = value
    27 
    31 
    28         super(FormError, self).__init__(error)
    32         super(FormError, self).__init__(error)
    29 
    33 
    30 class OrderForm (object) :
    34 class OrderForm (object) :
    31     """
    35     """
    41                 app             - bind this form to the app state (db etc)
    45                 app             - bind this form to the app state (db etc)
    42         """
    46         """
    43 
    47 
    44         self.app = app
    48         self.app = app
    45 
    49 
       
    50         # accumulated errors
       
    51         self.errors = collections.defaultdict(list)
       
    52 
    46     def defaults (self) :
    53     def defaults (self) :
    47         """
    54         """
    48             Update our attributes with default values
    55             Update our attributes with default values
    49         """
    56         """
    50 
    57 
    76                 required        - raise a FormError if no value present
    83                 required        - raise a FormError if no value present
    77 
    84 
    78             Returns the value as a str, or default
    85             Returns the value as a str, or default
    79         """
    86         """
    80 
    87 
    81         if name in self.post :
    88         if name in self.data :
    82             return self.post[name]
    89             return self.data[name]
    83 
    90 
    84         elif required :
    91         elif required :
    85             raise FormError(name, "Required field")
    92             raise FormError(name, None, "Required field")
    86 
    93 
    87         else :
    94         else :
    88             return default
    95             return default
    89 
    96 
    90     def process_string_field (self, name, default=None, required=None, strip=True) :
    97     def process_string_field (self, name, default=None, required=None, strip=True) :
   104         try :
   111         try :
   105             # XXX: decode somehow, or can werkzeug handle that?
   112             # XXX: decode somehow, or can werkzeug handle that?
   106             value = unicode(value)
   113             value = unicode(value)
   107 
   114 
   108         except UnicodeDecodeError :
   115         except UnicodeDecodeError :
   109             raise FormError(name, "Failed to decode Unicode characters")
   116             raise FormError(name, value, "Failed to decode Unicode characters")
   110 
   117 
   111         if strip :
   118         if strip :
   112             value = value.strip()
   119             value = value.strip()
   113 
   120 
   114         return value
   121         return value
   127 
   134 
   128         try :
   135         try :
   129             return int(value)
   136             return int(value)
   130 
   137 
   131         except ValueError :
   138         except ValueError :
   132             raise FormError(name, "Must be a number")
   139             raise FormError(name, value, "Must be a number")
   133     
   140     
   134     DATETIME_FORMAT = "%d.%m.%Y %H:%M"
   141     DATETIME_FORMAT = "%d.%m.%Y %H:%M"
   135 
   142 
   136     def process_datetime_field (self, name, default=None, required=None, format=DATETIME_FORMAT) :
   143     def process_datetime_field (self, name, default=None, required=None, format=DATETIME_FORMAT) :
   137         """
   144         """
   147 
   154 
   148         try :
   155         try :
   149             return datetime.datetime.strptime(value, format)
   156             return datetime.datetime.strptime(value, format)
   150 
   157 
   151         except ValueError, ex :
   158         except ValueError, ex :
   152             raise FormError(name, "Invalid date/time value: " + str(ex))
   159             raise FormError(name, value, "Invalid date/time value: " + str(ex))
   153 
   160 
   154     def process_multifield (self, table, id, fields) :
   161     def process_multifield (self, table, id, fields) :
   155         """
   162         """
   156             Process a set of user-given field values for an object with an unique id, and some set of additional fields.
   163             Process a set of user-given field values for an object with an unique id, and some set of additional fields.
   157             
   164             
   168             # look up object from db
   175             # look up object from db
   169             columns = [col for name, col, value in fields]
   176             columns = [col for name, col, value in fields]
   170 
   177 
   171             sql = db.select(columns, (id_col == id_value))
   178             sql = db.select(columns, (id_col == id_value))
   172 
   179 
   173             for row in sql :
   180             for row in self.app.query(sql) :
   174                 # XXX: sanity-check row values vs our values
   181                 # XXX: sanity-check row values vs our values
   175 
   182 
   176                 # new values
   183                 # new values
   177                 fields = tuple(
   184                 fields = tuple(
   178                     (name, col, row[col]) for name, col, value in fields
   185                     (name, col, row[col]) for name, col, value in fields
   181                 # ok, just use the first one
   188                 # ok, just use the first one
   182                 break
   189                 break
   183 
   190 
   184             else :
   191             else :
   185                 # not found!
   192                 # not found!
   186                 raise FormError(id_name, "Item selected does not seem to exist")
   193                 raise FormError(id_name, id_value, "Item selected does not seem to exist")
   187                 
   194                 
   188             log.info("Lookup %s=%d -> %s", id_name, id_value, dict((name, value) for name, col, value in fields))
   195             log.info("Lookup %s=%d -> %s", id_name, id_value, dict((name, value) for name, col, value in fields))
   189 
   196 
   190         elif any(value for name, col, value in fields) :
   197         elif any(value for name, col, value in fields) :
   191             # look up identical object from db?
   198             # look up identical object from db?
   218     def process_customer (self) :
   225     def process_customer (self) :
   219         """
   226         """
   220             Process the incoming customer_* fields, returning (customer_id, customer_name).
   227             Process the incoming customer_* fields, returning (customer_id, customer_name).
   221         """
   228         """
   222 
   229 
   223         return self.process_multifield(db.customers,
   230         try :
   224             ('customer_id', db.customers.c.id, self.process_integer_field('customer_id')),
   231             customer_id = self.process_integer_field('customer_id')
   225             (
   232             customer_name = self.process_string_field('customer_name')
   226                 ('customer_name', db.customers.c.name, self.process_string_field('customer_name')),
   233 
   227             ),
   234             if not customer_id and not customer_name :
   228         )
   235                 raise FormError('customer_name', None, "Must enter a customer")
   229 
   236        
       
   237             return self.process_multifield(db.customers,
       
   238                 ('customer_id', db.customers.c.id, customer_id),
       
   239                 (
       
   240                     ('customer_name', db.customers.c.name, customer_name),
       
   241                 ),
       
   242             )
       
   243 
       
   244         except FormError, e :
       
   245             # list it
       
   246             self.fail_field(e, 'customer_name')
       
   247 
       
   248             return None, None
       
   249  
   230     def process_contact (self, customer_id) :
   250     def process_contact (self, customer_id) :
   231         """
   251         """
   232             Process the incoming contact_* fields, returning 
   252             Process the incoming contact_* fields, returning 
   233                 (contact_id, contact_name, contact_phone, contact_email, contact_customer)
   253                 (contact_id, contact_name, contact_phone, contact_email, contact_customer)
   234         """
   254         """
   235 
   255         
   236         return self.process_multifield(db.contacts,
   256         try :
   237             ('contact_id', db.contacts.c.id, self.process_integer_field('contact_id')),
   257             contact_id = self.process_integer_field('contact_id')
   238             (
   258             contact_name = self.process_string_field('contact_name')
   239                 ('contact_name', db.contacts.c.name, self.process_string_field('contact_name')),
   259             contact_phone = self.process_string_field('contact_phone')
   240                 ('contact_phone', db.contacts.c.phone, self.process_string_field('contact_phone')),
   260             contact_email = self.process_string_field('contact_email')
   241                 ('contact_email', db.contacts.c.email, self.process_string_field('contact_email')),
   261             contact_customer = customer_id
   242                 ('contact_customer', db.contacts.c.customer, customer_id),
   262 
   243             ),
   263             if not contact_id and not (contact_name or contact_phone or contact_email) :
   244         )
   264                 raise FormError('contact_name', None, "Must enter a contact")
       
   265 
       
   266             return self.process_multifield(db.contacts,
       
   267                 ('contact_id', db.contacts.c.id, contact_id),
       
   268                 (
       
   269                     ('contact_name', db.contacts.c.name, contact_name),
       
   270                     ('contact_phone', db.contacts.c.phone, contact_phone),
       
   271                     ('contact_email', db.contacts.c.email, contact_email),
       
   272                     ('contact_customer', db.contacts.c.customer, contact_customer),
       
   273                 ),
       
   274             )
       
   275 
       
   276         except FormError, e :
       
   277             # list it
       
   278             self.fail_field(e, 'contact_name' if e.field == 'contact_id' else None)
       
   279 
       
   280             return None, None, None, None, None
   245 
   281 
   246     def process_event (self) :
   282     def process_event (self) :
   247         """
   283         """
   248             Process the incoming event_* fields, returning
   284             Process the incoming event_* fields, returning
   249                 (event_name, event_description, event_start, event_end)
   285                 (event_name, event_description, event_start, event_end)
   250         """
   286         """
   251 
   287         
   252         event_name = self.process_string_field('event_name')
   288         try :
   253         event_description = self.process_string_field('event_description', strip=False)
   289             event_name = self.process_string_field('event_name')
   254         event_start = self.process_datetime_field('event_start')
   290             event_description = self.process_string_field('event_description', strip=False)
   255         event_end = self.process_datetime_field('event_end')
   291             event_start = self.process_datetime_field('event_start')
   256 
   292             event_end = self.process_datetime_field('event_end')
   257         return (event_name, event_description, event_start, event_end)
   293 
       
   294             if event_end < event_start :
       
   295                 raise FormError('event_start', event_end, "Event must end after start")
       
   296 
       
   297             return (event_name, event_description, event_start, event_end)
       
   298 
       
   299         except FormError, e :
       
   300             # list it
       
   301             self.fail_field(e)
       
   302 
       
   303             return None, None, None, None
   258 
   304 
   259     def process (self, data) :
   305     def process (self, data) :
   260         """
   306         """
   261             Bind ourselves to the given incoming POST data, and update our order field attributes.
   307             Bind ourselves to the given incoming POST data, and update our order field attributes.
   262 
   308 
   263                 data        - the submitted POST data as a MultiDict
   309                 data        - the submitted POST data as a MultiDict
       
   310 
       
   311             Returns True if all fields were processed without errors, False otherwise.
   264         """
   312         """
   265 
   313 
   266         # bind the raw post data
   314         # bind the raw post data
   267         self.data = data
   315         self.data = data
   268 
   316 
   269         # customer
   317         # customer
   270         self.customer_id, self.customer_name = self.process_customer()
   318         self.customer_id, self.customer_name = self.process_customer()
   271 
   319 
   272         # contact
   320         # contact
   273         self.contact_id, self.contact_name, self.contact_phone, self.contact_email, self.contact_customer = self.process_contact(customer_id)
   321         self.contact_id, self.contact_name, self.contact_phone, self.contact_email, self.contact_customer = self.process_contact(self.customer_id)
   274 
   322 
   275         if self.contact_customer and not self.customer_id :
   323         if self.contact_customer and not self.customer_id :
   276             # TODO: be really smart?
   324             # TODO: be really smart?
   277             pass
   325             pass
   278 
   326 
   279         # event
   327         # event
   280         self.event_name, self.event_description, self.event_start, self.event_end = self.process_event()
   328         self.event_name, self.event_description, self.event_start, self.event_end = self.process_event()
   281 
   329     
       
   330         return not self.errors
       
   331 
       
   332     def fail_field (self, form_error, field=None) :
       
   333         """
       
   334             Mark the field mentioned inside the given FormError as failed.
       
   335 
       
   336                 form_error      - the FormError to store
       
   337                 field           - the name of the field to store the error under, if not the same as in form_error
       
   338         """
       
   339 
       
   340         field = field or form_error.field
       
   341 
       
   342         log.warn("Marking field %s as failed: %s", field, form_error)
       
   343 
       
   344         self.errors[field].append(form_error)
   282 
   345 
   283     def build_customer_list (self) :
   346     def build_customer_list (self) :
   284         """
   347         """
   285             Query a (id, name) list of customers.
   348             Query a (id, name) list of customers.
   286         """
   349         """
   351         """
   414         """
   352             Render HTML for customer_id/name field inputs.
   415             Render HTML for customer_id/name field inputs.
   353         """
   416         """
   354 
   417 
   355         # all known customers
   418         # all known customers
   356         customers = self.build_customer_list()
   419         customers = list(self.build_customer_list())
   357         
   420         
   358         return (
   421         return (
   359             self.render_select_input('customer_id', customers, self.customer_id),
   422             self.render_select_input('customer_id', [(0, u"Luo uusi")] + customers, self.customer_id),
   360             self.render_text_input('customer_name', self.customer_name),
   423             self.render_text_input('customer_name', self.customer_name),
   361 
   424 
   362             tags.script(r"$(document).ready(function () { $('#customer_id').formSelectPreset({textTarget: $('#customer_name')}); });"),
   425             tags.script(r"$(document).ready(function () { $('#customer_id').formSelectPreset({textTarget: $('#customer_name')}); });"),
   363         )
   426         )
   364 
   427 
   368         """
   431         """
   369         # recommended contacts for selected customer, if known
   432         # recommended contacts for selected customer, if known
   370         contacts = self.build_contact_list(self.customer_id)
   433         contacts = self.build_contact_list(self.customer_id)
   371 
   434 
   372         return (
   435         return (
   373             self.render_select_input('contact_id', ((id, name) for id, name, phone, email in contacts), self.contact_id),
   436             self.render_select_input('contact_id', [(0, u"Luo uusi")] + [(id, name) for id, name, phone, email in contacts], self.contact_id),
   374             self.render_text_input('contact_name', self.contact_name),
   437             self.render_text_input('contact_name', self.contact_name),
   375 
   438 
   376             tags.script(r"$(document).ready(function () { $('#contact_id').formSelectPreset({textTarget: $('#contact_name')}); });"),
   439             tags.script(r"$(document).ready(function () { $('#contact_id').formSelectPreset({textTarget: $('#contact_name')}); });"),
   377         )
   440         )
   378 
   441 
   417     def render_form_field (self, name, title, description, inputs) :
   480     def render_form_field (self, name, title, description, inputs) :
   418         """
   481         """
   419             Render the label, input control, error note and description for a single field, along with their containing <li>.
   482             Render the label, input control, error note and description for a single field, along with their containing <li>.
   420         """
   483         """
   421 
   484 
   422         return tags.li(class_='field')(
   485         # any errors for this field?
   423             tags.label(title, for_=name),
   486         errors = self.errors[name]
       
   487 
       
   488         return tags.li(class_='field' + (' failed' if errors else ''))(
       
   489             tags.label((
       
   490                 title,
       
   491                 tags.strong(u"(Virheellinen)") if errors else None,
       
   492             ), for_=name),
   424 
   493 
   425             inputs,
   494             inputs,
   426 
   495 
   427             # XXX: somewhere where we tag these!
       
   428             # tags.span("Error!"),
       
   429 
       
   430             tags.p(description),
   496             tags.p(description),
       
   497             
       
   498             # possible errors
       
   499             tags.ul(class_='errors')(tags.li(error.message) for error in errors) if errors else None,
   431         )
   500         )
   432 
   501 
   433 
   502 
   434     def render (self, action, submit=u"Tallenna") :
   503     def render (self, action, submit=u"Tallenna") :
   435         """
   504         """
   438                 action          - the target URL for the form to POST to
   507                 action          - the target URL for the form to POST to
   439                 submit          - label for the submit button
   508                 submit          - label for the submit button
   440         """
   509         """
   441 
   510 
   442         return tags.form(action=action, method='POST')(
   511         return tags.form(action=action, method='POST')(
       
   512             (
       
   513                 tags.h3(u"Lomakkeessa oli virheitä"),
       
   514                 tags.p(u"Korjaa lomake ja lähetä uudelleen"),
       
   515 
       
   516                 tags.ul(class_='errors')(
       
   517                     tags.li(tags.a(href='#' + error.field)(
       
   518                         tags.em(error.field),
       
   519                         error.message,
       
   520                     )) for field_errors in self.errors.itervalues() for error in field_errors
       
   521                 ),
       
   522             ) if self.errors else None,
       
   523 
   443             tags.fieldset(
   524             tags.fieldset(
   444                 tags.legend(u"Tilaaja"),
   525                 tags.legend(u"Tilaaja"),
   445                 
   526                 
   446                 tags.ol(
   527                 tags.ol(
   447                     self.render_form_field('customer_name', u"Tilaaja", u"Tilaavan yhdistyksen/henkilön nimi", (
   528                     self.render_form_field('customer_name', u"Tilaaja", u"Tilaavan yhdistyksen/henkilön nimi", (
   492     def render_content (self, id) :
   573     def render_content (self, id) :
   493         return tags.h1("Order info for #%d" % (id, ))
   574         return tags.h1("Order info for #%d" % (id, ))
   494 
   575 
   495 class NewOrderView (PageHandler) :
   576 class NewOrderView (PageHandler) :
   496     """
   577     """
       
   578         Render form for input, let the user correct their errors, create the order, and redirect out.
       
   579     """
       
   580 
       
   581     def create (self, form) :
       
   582         """
       
   583             Create the new order from the given form data, returning the new order's ID
       
   584         """
       
   585 
       
   586         # if we've gotten this far, then we can create it!
       
   587         sql = db.insert(db.orders).values(
       
   588             customer            = form.customer_id,
       
   589             contact             = form.contact_id,
   497             
   590             
   498     """
   591             event_name          = form.event_name,
   499 
   592             event_description   = form.event_description,
       
   593             event_start         = form.event_start,
       
   594             event_end           = form.event_end,
       
   595         )
       
   596 
       
   597         # go!
       
   598         order_id, = self.app.insert(sql)
       
   599 
       
   600         # great
       
   601         return order_id
       
   602 
       
   603     def process (self) :
       
   604         """
       
   605             Set up up our form.
       
   606         """
       
   607 
       
   608         self.form = OrderForm(self.app)
       
   609 
       
   610         # use either POST data or defaults
       
   611         if self.POST :
       
   612             # try and process the input, checking for any failures...
       
   613             if self.form.process(self.POST) :
       
   614                 # should be good, create it!
       
   615                 order_id = self.create(self.form)
       
   616                 
       
   617                 # redirect there now that our business is done and the order exists
       
   618                 return self.redirect_for(OrderView, id=order_id)
       
   619             
       
   620             else :
       
   621                 # errors in form input
       
   622                 pass
       
   623 
       
   624         else :
       
   625             # init from defaults
       
   626             self.form.defaults()
       
   627     
   500     def render_content (self) :
   628     def render_content (self) :
   501 
   629         """
   502         form = OrderForm(self.app)
   630             Render our form
   503         form.defaults()
   631         """
   504 
   632 
   505         return form.render(action=self.url_for(NewOrderView))
   633         return (
   506 
   634             tags.h1(u"Uusi tilaus"),
   507         # XXX: under construction..
   635             self.form.render(action=self.url_for(NewOrderView))
   508 
   636         )
   509         if self.POST :
   637 
   510             print self.POST
       
   511 
       
   512             
       
   513             # if we've gotten this far, then we can create it!
       
   514             sql = db.insert(db.orders).values(
       
   515                 customer            = customer_id,
       
   516                 contact             = contact_id,
       
   517                 
       
   518                 event_name          = event_name,
       
   519                 event_description   = event_description,
       
   520                 event_start         = event_start,
       
   521                 event_end           = event_end,
       
   522             )
       
   523 
       
   524             # go!
       
   525             order_id, = self.app.insert(sql)
       
   526 
       
   527             # ok, we don't need the /new URL anymore, we can just show the order page
       
   528             return self.redirect_for(OrderView, id=order_id)
       
   529 
       
   530 
       
   531