changeset 11 | 90a3c570c227 |
parent 10 | 4bdb45071c89 |
child 12 | 2d3fb967cd30 |
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 |