6 from svv.controllers import PageHandler |
6 from svv.controllers import PageHandler |
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 |
11 import logging |
12 class OrdersView (PageHandler) : |
12 |
13 def render (self) : |
13 log = logging.getLogger('svv.orders') |
14 return tags.h1("Orders list") |
14 |
15 |
15 class FormError (Exception) : |
16 class OrderView (PageHandler) : |
16 """ |
17 def render (self) : |
17 A user-level error in a form field |
18 return tags.h1("Order info") |
18 """ |
19 |
19 |
20 class NewOrderView (PageHandler) : |
20 def __init__ (self, field, error) : |
21 def render_form_field (self, title, description, name, input) : |
21 """ |
22 return tags.li(class_='field')( |
22 field - name of field with error |
23 tags.label(title, for_=name), |
23 error - descriptive text for user |
24 |
24 """ |
25 input, |
25 |
26 |
26 self.field = field |
27 # tags.span("Error!"), |
27 |
28 |
28 super(FormError, self).__init__(error) |
29 tags.p(description), |
29 |
30 ) |
30 class OrderForm (object) : |
31 |
31 """ |
32 def get_customer_list (self) : |
32 A single instance of a <form>, where we can process submitted data from the client, storing the associated |
33 """ |
33 Order-related data, and then render a form for any of that related data. |
34 Get (id, name) list of customers |
34 """ |
35 """ |
35 |
36 |
36 # any POST data we have processed, updated from process() |
37 return self.app.query(db.select([db.customers.c.id, db.customers.c.name])) |
37 data = None |
38 |
38 |
39 def get_contact_list (self, customer_id=None) : |
39 def __init__ (self, app) : |
40 """ |
40 """ |
41 Get (id, name, phone, email) list of contacts, optionally for given customer if given. |
41 app - bind this form to the app state (db etc) |
42 """ |
42 """ |
43 |
43 |
44 query = db.select([db.contacts.c.id, db.contacts.c.name, db.contacts.c.phone, db.contacts.c.email]) |
44 self.app = app |
|
45 |
|
46 def defaults (self) : |
|
47 """ |
|
48 Update our attributes with default values |
|
49 """ |
|
50 |
|
51 self.customer_id = None |
|
52 self.customer_name = None |
|
53 |
|
54 self.contact_id = None |
|
55 self.contact_name = None |
|
56 self.contact_phone = None |
|
57 self.contact_email = None |
|
58 self.contact_customer = None |
|
59 |
|
60 self.event_name = None |
|
61 self.event_description = None |
|
62 |
|
63 tomorrow = datetime.date.today() + datetime.timedelta(days=1) |
|
64 |
|
65 # default to tomorrow afternoon |
|
66 self.event_start = datetime.datetime.combine(tomorrow, datetime.time(16, 00)) |
|
67 |
|
68 # automatically determined once start is set |
|
69 self.event_end = None |
|
70 |
|
71 def process_raw_field (self, name, default=None, required=None) : |
|
72 """ |
|
73 Process a generic incoming data field. |
|
74 |
|
75 default - value to return if no value was present |
|
76 required - raise a FormError if no value present |
|
77 |
|
78 Returns the value as a str, or default |
|
79 """ |
|
80 |
|
81 if name in self.post : |
|
82 return self.post[name] |
|
83 |
|
84 elif required : |
|
85 raise FormError(name, "Required field") |
|
86 |
|
87 else : |
|
88 return default |
|
89 |
|
90 def process_string_field (self, name, default=None, required=None, strip=True) : |
|
91 """ |
|
92 Process a generic incoming string field. |
|
93 |
|
94 Trims extra whitespace from around the value, unless strip=False is given. |
|
95 |
|
96 Returns the value as unicode, or default. |
|
97 """ |
|
98 |
|
99 value = self.process_raw_field(name, required=required) |
|
100 |
|
101 if value is None : |
|
102 return default |
|
103 |
|
104 try : |
|
105 # XXX: decode somehow, or can werkzeug handle that? |
|
106 value = unicode(value) |
|
107 |
|
108 except UnicodeDecodeError : |
|
109 raise FormError(name, "Failed to decode Unicode characters") |
|
110 |
|
111 if strip : |
|
112 value = value.strip() |
|
113 |
|
114 return value |
|
115 |
|
116 def process_integer_field (self, name, default=None, required=None) : |
|
117 """ |
|
118 Process a generic incoming int field. |
|
119 |
|
120 Returns the value as int, or default. |
|
121 """ |
|
122 |
|
123 value = self.process_raw_field(name, required=required) |
|
124 |
|
125 if value is None : |
|
126 return default |
|
127 |
|
128 try : |
|
129 return int(value) |
|
130 |
|
131 except ValueError : |
|
132 raise FormError(name, "Must be a number") |
|
133 |
|
134 DATETIME_FORMAT = "%d.%m.%Y %H:%M" |
|
135 |
|
136 def process_datetime_field (self, name, default=None, required=None, format=DATETIME_FORMAT) : |
|
137 """ |
|
138 Process an incoming datetime field. |
|
139 |
|
140 Returns the value as datetime, or default. |
|
141 """ |
|
142 |
|
143 value = self.process_raw_field(name, required=required) |
|
144 |
|
145 if value is None : |
|
146 return default |
|
147 |
|
148 try : |
|
149 return datetime.datetime.strptime(value, format) |
|
150 |
|
151 except ValueError, ex : |
|
152 raise FormError(name, "Invalid date/time value: " + str(ex)) |
|
153 |
|
154 def process_multifield (self, table, id, fields) : |
|
155 """ |
|
156 Process a set of user-given field values for an object with an unique id, and some set of additional fields. |
|
157 |
|
158 If the id is given, look up the corresponding field values, and return those. |
|
159 |
|
160 If any of the fields are given, either look up a matching row, or create a new one, and return its id. |
|
161 |
|
162 Returns an (id_name, field_name, ...) N-tuple. |
|
163 """ |
|
164 |
|
165 id_name, id_col, id_value = id |
|
166 |
|
167 if id_value : |
|
168 # look up object from db |
|
169 columns = [col for name, col, value in fields] |
|
170 |
|
171 sql = db.select(columns, (id_col == id_value)) |
|
172 |
|
173 for row in sql : |
|
174 # XXX: sanity-check row values vs our values |
|
175 |
|
176 # new values |
|
177 fields = tuple( |
|
178 (name, col, row[col]) for name, col, value in fields |
|
179 ) |
|
180 |
|
181 # ok, just use the first one |
|
182 break |
|
183 |
|
184 else : |
|
185 # not found! |
|
186 raise FormError(id_name, "Item selected does not seem to exist") |
|
187 |
|
188 log.info("Lookup %s=%d -> %s", id_name, id_value, dict((name, value) for name, col, value in fields)) |
|
189 |
|
190 elif any(value for name, col, value in fields) : |
|
191 # look up identical object from db? |
|
192 sql = db.select([id_col], db.and_(*[(col == value) for name, col, value in fields])) |
|
193 |
|
194 for row in self.app.query(sql) : |
|
195 if id_value : |
|
196 log.warn("Duplicate %s=%d for %s", id_name, id_value, dict((name, value) for name, col, value in fields)) |
|
197 |
|
198 # found object's id |
|
199 id_value = row[id_col] |
|
200 |
|
201 log.info("Found %s -> %d", dict((name, value) for name, col, value in fields), id_value) |
|
202 |
|
203 # create new object? |
|
204 if not id_value : |
|
205 sql = db.insert(table).values(dict((col, value) for name, col, value in fields)) |
|
206 |
|
207 id_value, = self.app.insert(sql) |
|
208 |
|
209 log.info("Create %s -> %d", dict((name, value) for name, col, value in fields), id_value) |
|
210 |
|
211 else : |
|
212 # not known |
|
213 log.debug("No %s known for order...", id_name) |
|
214 |
|
215 # return full set of values |
|
216 return (id_value, ) + tuple(value for name, col, value in fields) |
|
217 |
|
218 def process_customer (self) : |
|
219 """ |
|
220 Process the incoming customer_* fields, returning (customer_id, customer_name). |
|
221 """ |
|
222 |
|
223 return self.process_multifield(db.customers, |
|
224 ('customer_id', db.customers.c.id, self.process_integer_field('customer_id')), |
|
225 ( |
|
226 ('customer_name', db.customers.c.name, self.process_string_field('customer_name')), |
|
227 ), |
|
228 ) |
|
229 |
|
230 def process_contact (self, customer_id) : |
|
231 """ |
|
232 Process the incoming contact_* fields, returning |
|
233 (contact_id, contact_name, contact_phone, contact_email, contact_customer) |
|
234 """ |
|
235 |
|
236 return self.process_multifield(db.contacts, |
|
237 ('contact_id', db.contacts.c.id, self.process_integer_field('contact_id')), |
|
238 ( |
|
239 ('contact_name', db.contacts.c.name, self.process_string_field('contact_name')), |
|
240 ('contact_phone', db.contacts.c.phone, self.process_string_field('contact_phone')), |
|
241 ('contact_email', db.contacts.c.email, self.process_string_field('contact_email')), |
|
242 ('contact_customer', db.contacts.c.customer, customer_id), |
|
243 ), |
|
244 ) |
|
245 |
|
246 def process_event (self) : |
|
247 """ |
|
248 Process the incoming event_* fields, returning |
|
249 (event_name, event_description, event_start, event_end) |
|
250 """ |
|
251 |
|
252 event_name = self.process_string_field('event_name') |
|
253 event_description = self.process_string_field('event_description', strip=False) |
|
254 event_start = self.process_datetime_field('event_start') |
|
255 event_end = self.process_datetime_field('event_end') |
|
256 |
|
257 return (event_name, event_description, event_start, event_end) |
|
258 |
|
259 def process (self, data) : |
|
260 """ |
|
261 Bind ourselves to the given incoming POST data, and update our order field attributes. |
|
262 |
|
263 data - the submitted POST data as a MultiDict |
|
264 """ |
|
265 |
|
266 # bind the raw post data |
|
267 self.data = data |
|
268 |
|
269 # customer |
|
270 self.customer_id, self.customer_name = self.process_customer() |
|
271 |
|
272 # contact |
|
273 self.contact_id, self.contact_name, self.contact_phone, self.contact_email, self.contact_customer = self.process_contact(customer_id) |
|
274 |
|
275 if self.contact_customer and not self.customer_id : |
|
276 # TODO: be really smart? |
|
277 pass |
|
278 |
|
279 # event |
|
280 self.event_name, self.event_description, self.event_start, self.event_end = self.process_event() |
|
281 |
|
282 |
|
283 def build_customer_list (self) : |
|
284 """ |
|
285 Query a (id, name) list of customers. |
|
286 """ |
|
287 |
|
288 sql = db.select([db.customers.c.id, db.customers.c.name]) |
|
289 |
|
290 return self.app.query(sql) |
|
291 |
|
292 def build_contact_list (self, customer_id=None) : |
|
293 """ |
|
294 Query a (id, name, phone, email) list of contacts, optionally for given customer if given. |
|
295 """ |
|
296 |
|
297 sql = db.select([db.contacts.c.id, db.contacts.c.name, db.contacts.c.phone, db.contacts.c.email]) |
45 |
298 |
46 if customer_id : |
299 if customer_id : |
47 query = query.where((db.contacts.c.customer == customer_id)) |
300 sql = sql.where((db.contacts.c.customer == customer_id)) |
48 |
301 |
49 return self.app.query(query) |
302 return self.app.query(sql) |
|
303 |
|
304 |
|
305 def render_text_input (self, name, value=None, multiline=False) : |
|
306 """ |
|
307 Render HTML for a generic text field input. |
|
308 |
|
309 name - field name, as used for POST |
|
310 value - default field value |
|
311 multiline - use a multi-line <textarea> instead |
|
312 """ |
|
313 |
|
314 if multiline : |
|
315 # XXX: textarea can't be self-closing for some reason? |
|
316 return tags.textarea(name=name, id=name, _selfclosing=False, _whitespace_sensitive=True)(value) |
|
317 |
|
318 else : |
|
319 return tags.input(type='text', name=name, id=name, value=value) |
|
320 |
|
321 def render_select_input (self, name, options, value=None) : |
|
322 """ |
|
323 Render HTML for a generic select control. |
|
324 |
|
325 name - field name, as used for POST |
|
326 options - sequence of (value, title) options. `value` may be None to omit. |
|
327 value - the selected value |
|
328 """ |
|
329 |
|
330 return tags.select(name=name, id=name)( |
|
331 ( |
|
332 tags.option(value=opt_value, selected=('selected' if opt_value == value else None))(opt_title) |
|
333 ) for opt_value, opt_title in options |
|
334 ) |
50 |
335 |
51 def render_customer_input (self) : |
336 def render_customer_input (self) : |
52 """ |
337 """ |
53 Render HTML for customer field <input>s |
338 Render HTML for customer_id/name field inputs. |
54 """ |
339 """ |
55 |
340 |
56 # pre-selected values? |
341 # all known customers |
57 customer_id = self.POST.get('customer_id') |
342 customers = self.build_customer_list() |
58 customer_name = self.POST.get('customer_name') |
|
59 |
|
60 # available values |
|
61 customers = self.get_customer_list() |
|
62 |
343 |
63 return ( |
344 return ( |
64 tags.select(name='customer_id', id='customer_id')( |
345 self.render_select_input('customer_id', customers, self.customer_id), |
65 tags.option(value=0)(u"Luo uusi"), |
346 self.render_text_input('customer_name', self.customer_name), |
66 |
|
67 [( |
|
68 tags.option(value=id, selected=('selected' if id == customer_id or name == customer_name else None))(name) |
|
69 ) for id, name in customers], |
|
70 ), |
|
71 tags.input(type='text', name='customer_name', id='customer_name'), |
|
72 |
347 |
73 tags.script(r"$(document).ready(function () { $('#customer_id').formSelectPreset({textTarget: $('#customer_name')}); });"), |
348 tags.script(r"$(document).ready(function () { $('#customer_id').formSelectPreset({textTarget: $('#customer_name')}); });"), |
74 ) |
349 ) |
75 |
350 |
76 def render_contact_input (self) : |
351 def render_contact_input (self) : |
77 """ |
352 """ |
78 Render HTML for contact name field <input>s |
353 Render HTML for contact name field <input>s |
79 """ |
354 """ |
80 |
355 # recommended contacts for selected customer, if known |
81 # pre-selected values |
356 contacts = self.get_contact_list(self.customer_id) |
82 customer_id = self.POST.get('customer_id') |
|
83 contact_id = self.POST.get('contact_id') |
|
84 contact_name = self.POST.get('contact_name') |
|
85 contact_phone = self.POST.get('contact_phone') |
|
86 contact_email = self.POST.get('contact_email') |
|
87 |
|
88 # available values |
|
89 contacts = self.get_contact_list() |
|
90 |
357 |
91 return ( |
358 return ( |
92 tags.select(name='contact_id', id='contact_id')( |
359 self.render_select_input('contact_id', ((id, name) for id, name, phone, email in contacts), self.contact_id) |
93 tags.option(value=0)(u"Luo uusi"), |
360 self.render_text_input('contact_name', self.contact_name), |
94 |
|
95 [( |
|
96 tags.option(value=id, selected=('selected' if id == contact_id else None))(name) |
|
97 ) for id, name, phone, email in contacts], |
|
98 ), |
|
99 |
|
100 tags.input(type='text', name='contact_name', id='contact_name'), |
|
101 |
361 |
102 tags.script(r"$(document).ready(function () { $('#contact_id').formSelectPreset({textTarget: $('#contact_name')}); });"), |
362 tags.script(r"$(document).ready(function () { $('#contact_id').formSelectPreset({textTarget: $('#contact_name')}); });"), |
103 ) |
363 ) |
104 |
364 |
105 DATETIME_FORMAT = "%d.%m.%Y %H:%M" |
|
106 |
|
107 def get_POST_datetime (self, name, default=None) : |
|
108 """ |
|
109 Return a datetime for something the client POST'd |
|
110 """ |
|
111 |
|
112 value = self.POST.get(name) |
|
113 |
|
114 if value : |
|
115 # XXX: handle invalid format.. |
|
116 return datetime.datetime.strptime(value, self.DATETIME_FORMAT) |
|
117 |
|
118 else : |
|
119 return default |
|
120 |
|
121 def render_event_input (self) : |
365 def render_event_input (self) : |
122 """ |
366 """ |
123 Render HTML for event start/end field <input>s |
367 Render HTML for event start/end field <input>s |
124 """ |
368 """ |
125 |
369 |
126 # XXX: sensible defaults? |
|
127 tomorrow = datetime.date.today() + datetime.timedelta(days=1) |
|
128 default_start = datetime.datetime.combine(tomorrow, datetime.time(16, 00)) |
|
129 |
|
130 # automatically determined once start is set |
|
131 default_end = None |
|
132 |
|
133 # pre-selected values |
|
134 event_start = self.get_POST_datetime('event_start', default_start) |
|
135 event_end = self.get_POST_datetime('event_end', default_end) |
|
136 |
|
137 return ( |
370 return ( |
138 tags.input(type='text', name='event_start', id='event_start', value=(event_start.strftime(self.DATETIME_FORMAT) if event_start else None)), |
371 self.render_text_input('event_start', (self.event_start.strftime(self.DATETIME_FORMAT) if event_start else None)), |
139 " - ", |
372 " - ", |
140 tags.input(type='text', name='event_end', id='event_end', value=(event_end.strftime(self.DATETIME_FORMAT) if event_end else None)), |
373 self.render_text_input('event_end', (self.event_end.strftime(self.DATETIME_FORMAT) if event_end else None)), |
141 |
374 |
142 tags.script(r""" |
375 tags.script(r""" |
143 $(document).ready(function () { |
376 $(document).ready(function () { |
144 var event_start = $('#event_start'); |
377 var event_start = $('#event_start'); |
145 var event_end = $('#event_end'); |
378 var event_end = $('#event_end'); |
146 |
379 |
147 event_start.datetimepicker(); |
380 event_start.datetimepicker(); |
148 event_end.datetimepicker(); |
381 event_end.datetimepicker(); |
149 |
382 |
|
383 /* Buggy shit doesn't work |
|
384 |
|
385 { |
|
386 beforeShow: function (input, inst) { |
|
387 // copy default value from event_start |
|
388 event_end.datetimepicker("option", "defaultDate", event_start.datetimepicker("getDate")); |
|
389 } |
|
390 } |
|
391 |
150 event_start.change(function () { |
392 event_start.change(function () { |
151 // copy value as default |
393 // copy value as default |
152 var start_date = event_start.datetimepicker("getDate"); |
394 var start_date = event_start.datetimepicker("getDate"); |
153 |
395 |
154 event_end.datetimepicker("option", "defaultDate", start_date); |
396 event_end.datetimepicker("option", "defaultDate", start_date); |
155 }); |
397 }); |
156 |
398 |
157 // init default as well |
399 // init default as well |
158 event_start.change(); |
400 event_start.change(); |
|
401 */ |
159 });""" ), |
402 });""" ), |
160 |
403 |
161 ) |
404 ) |
162 |
405 |
163 def render (self) : |
406 def render_form_field (self, name, title, description, inputs) : |
164 return tags.form(action=self.build_url(NewOrderView), method='POST')( |
407 """ |
165 tags.h1(u"Uusi tilaus"), |
408 Render the label, input control, error note and description for a single field, along with their containing <li>. |
166 |
409 """ |
|
410 |
|
411 return tags.li(class_='field')( |
|
412 tags.label(title, for_=name), |
|
413 |
|
414 inputs, |
|
415 |
|
416 # XXX: somewhere where we tag these! |
|
417 # tags.span("Error!"), |
|
418 |
|
419 tags.p(description), |
|
420 ) |
|
421 |
|
422 |
|
423 def render (self, action, submit=u"Tallenna") : |
|
424 """ |
|
425 Render the entire <form>, using any loaded/processed values. |
|
426 |
|
427 action - the target URL for the form to POST to |
|
428 submit - label for the submit button |
|
429 """ |
|
430 |
|
431 return tags.form(action=action, method='POST')( |
167 tags.fieldset( |
432 tags.fieldset( |
168 tags.legend(u"Tilaaja"), |
433 tags.legend(u"Tilaaja"), |
169 |
434 |
170 tags.ol( |
435 tags.ol( |
171 self.render_form_field(u"Tilaaja", u"Tilaavan yhdistyksen/henkilön nimi", 'customer_name', self.render_customer_input()), |
436 self.render_form_field('customer_name', u"Tilaaja", u"Tilaavan yhdistyksen/henkilön nimi", ( |
172 |
437 self.render_customer_input() |
173 self.render_form_field(u"Yhteyshenkilö", u"Yhteyshenkilön nimi, jos eri kun tilaaja", 'contact_name', self.render_contact_input()), |
438 )), |
174 |
439 |
175 self.render_form_field(u"Puhelin", u"Yhteyshenkilön puhelinnumero", 'contact_phone', ( |
440 self.render_form_field('contact_name', u"Yhteyshenkilö", u"Yhteyshenkilön nimi, jos eri kun tilaaja", ( |
176 tags.input(type='text', name='contact_phone') |
441 self.render_contact_input() |
177 )), |
442 )), |
178 |
443 |
179 self.render_form_field(u"Sähköposti", u"Yhteyshenkilön sähköpostiosoite", 'contact_email', ( |
444 self.render_form_field('contact_phone', u"Puhelin", u"Yhteyshenkilön puhelinnumero", ( |
180 tags.input(type='text') |
445 self.render_text_input('contact_phone', self.contact_phone) |
|
446 )), |
|
447 |
|
448 self.render_form_field('contact_email', u"Sähköposti", u"Yhteyshenkilön sähköpostiosoite", ( |
|
449 self.render_text_input('contact_email', self.contact_email) |
181 )), |
450 )), |
182 ), |
451 ), |
183 ), |
452 ), |
184 |
453 |
185 tags.fieldset( |
454 tags.fieldset( |
186 tags.legend(u"Tapahtuma"), |
455 tags.legend(u"Tapahtuma"), |
187 |
456 |
188 tags.ol( |
457 tags.ol( |
189 self.render_form_field(u"Tapahtuma", u"Tapahtuman lyhyt nimi", 'event_name', ( |
458 self.render_form_field('event_name', u"Tapahtuma", u"Tapahtuman lyhyt nimi", ( |
190 tags.input(type='text', name='event_name') |
459 self.render_text_input('event_name', self.event_name) |
191 )), |
460 )), |
192 |
461 |
193 self.render_form_field(u"Lisätiedot", u"Tapahtuman tarkemmat tiedot", 'event_description', ( |
462 self.render_form_field('event_description', u"Lisätiedot", u"Tapahtuman tarkemmat tiedot", ( |
194 tags.textarea("", rows=8, name='event_description') |
463 self.render_text_input('event_description', self.event_description, multiline=True) |
195 )), |
464 )), |
196 |
465 |
197 self.render_form_field(u"Ajankohta", u"Tapahtuman ajankohta (kamat noudetaan - palautetaan)", 'event_start', self.render_event_input()), |
466 self.render_form_field('event_start', u"Ajankohta", u"Tapahtuman ajankohta (kamat noudetaan - palautetaan)", ( |
|
467 self.render_event_input() |
|
468 )), |
198 ), |
469 ), |
199 ), |
470 ), |
200 |
471 |
201 tags.input(type='submit', value="Tallenna"), |
472 tags.input(type='submit', value=submit), |
202 ) |
473 ) |
203 |
474 |
|
475 |
|
476 class OrdersView (PageHandler) : |
|
477 def render (self) : |
|
478 return tags.h1("Orders list") |
|
479 |
|
480 class OrderView (PageHandler) : |
|
481 def render (self, id) : |
|
482 return tags.h1("Order info for #%d" % (id, )) |
|
483 |
|
484 class NewOrderView (PageHandler) : |
|
485 """ |
|
486 |
|
487 """ |
|
488 |
|
489 def render (self) : |
|
490 if self.POST : |
|
491 print self.POST |
|
492 |
|
493 # |
|
494 |
|
495 # if we've gotten this far, then we can create it! |
|
496 sql = db.insert(db.orders).values( |
|
497 customer = customer_id, |
|
498 contact = contact_id, |
|
499 |
|
500 event_name = event_name, |
|
501 event_description = event_description, |
|
502 event_start = event_start, |
|
503 event_end = event_end, |
|
504 ) |
|
505 |
|
506 # go! |
|
507 order_id, = self.app.insert(sql) |
|
508 |
|
509 # ok, we don't need the /new URL anymore, we can just show the order page |
|
510 return self.redirect_for(OrderView, id=order_id) |
|
511 |
|
512 # render form |
|
513 return self.render_form() |
|
514 |
|
515 |