1 # $Id: cgi_client.py,v 1.12 2001-07-30 06:26:31 richard Exp $
3 import os, cgi, pprint, StringIO, urlparse, re, traceback
5 import roundupdb, htmltemplate, date
7 class Unauthorised(ValueError):
8 pass
10 class Client:
11 def __init__(self, out, db, env, user):
12 self.out = out
13 self.db = db
14 self.env = env
15 self.user = user
16 self.path = env['PATH_INFO']
17 self.split_path = self.path.split('/')
19 self.headers_done = 0
20 self.form = cgi.FieldStorage(environ=env)
21 self.headers_done = 0
22 self.debug = 0
24 def header(self, headers={'Content-Type':'text/html'}):
25 if not headers.has_key('Content-Type'):
26 headers['Content-Type'] = 'text/html'
27 for entry in headers.items():
28 self.out.write('%s: %s\n'%entry)
29 self.out.write('\n')
30 self.headers_done = 1
32 def pagehead(self, title, message=None):
33 url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
34 machine = self.env['SERVER_NAME']
35 port = self.env['SERVER_PORT']
36 if port != '80': machine = machine + ':' + port
37 base = urlparse.urlunparse(('http', machine, url, None, None, None))
38 if message is not None:
39 message = '<div class="system-msg">%s</div>'%message
40 else:
41 message = ''
42 style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
43 userid = self.db.user.lookup(self.user)
44 self.write('''<html><head>
45 <title>%s</title>
46 <style type="text/css">%s</style>
47 </head>
48 <body bgcolor=#ffffff>
49 %s
50 <table width=100%% border=0 cellspacing=0 cellpadding=2>
51 <tr class="location-bar"><td><big><strong>%s</strong></big>
52 (login: <a href="user%s">%s</a>)</td></tr>
53 </table>
54 '''%(title, style, message, title, userid, self.user))
56 def pagefoot(self):
57 if self.debug:
58 self.write('<hr><small><dl>')
59 self.write('<dt><b>Path</b></dt>')
60 self.write('<dd>%s</dd>'%(', '.join(map(repr, self.split_path))))
61 keys = self.form.keys()
62 keys.sort()
63 if keys:
64 self.write('<dt><b>Form entries</b></dt>')
65 for k in self.form.keys():
66 v = str(self.form[k].value)
67 self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
68 keys = self.env.keys()
69 keys.sort()
70 self.write('<dt><b>CGI environment</b></dt>')
71 for k in keys:
72 v = self.env[k]
73 self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
74 self.write('</dl></small>')
75 self.write('</body></html>')
77 def write(self, content):
78 if not self.headers_done:
79 self.header()
80 self.out.write(content)
82 def index_arg(self, arg):
83 ''' handle the args to index - they might be a list from the form
84 (ie. submitted from a form) or they might be a command-separated
85 single string (ie. manually constructed GET args)
86 '''
87 if self.form.has_key(arg):
88 arg = self.form[arg]
89 if type(arg) == type([]):
90 return [arg.value for arg in arg]
91 return arg.value.split(',')
92 return []
94 def index_filterspec(self):
95 ''' pull the index filter spec from the form
97 Links and multilinks want to be lists - the rest are straight
98 strings.
99 '''
100 props = self.db.classes[self.classname].getprops()
101 # all the form args not starting with ':' are filters
102 filterspec = {}
103 for key in self.form.keys():
104 if key[0] == ':': continue
105 prop = props[key]
106 value = self.form[key]
107 if prop.isLinkType or prop.isMultilinkType:
108 if type(value) == type([]):
109 value = [arg.value for arg in value]
110 else:
111 value = value.value.split(',')
112 l = filterspec.get(key, [])
113 l = l + value
114 filterspec[key] = l
115 else:
116 filterspec[key] = value.value
117 return filterspec
119 default_index_sort = ['-activity']
120 default_index_group = ['priority']
121 default_index_filter = []
122 default_index_columns = ['id','activity','title','status','assignedto']
123 default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
124 def index(self):
125 ''' put up an index
126 '''
127 self.classname = 'issue'
128 if self.form.has_key(':sort'): sort = self.index_arg(':sort')
129 else: sort = self.default_index_sort
130 if self.form.has_key(':group'): group = self.index_arg(':group')
131 else: group = self.default_index_group
132 if self.form.has_key(':filter'): filter = self.index_arg(':filter')
133 else: filter = self.default_index_filter
134 if self.form.has_key(':columns'): columns = self.index_arg(':columns')
135 else: columns = self.default_index_columns
136 filterspec = self.index_filterspec()
137 if not filterspec:
138 filterspec = self.default_index_filterspec
139 return self.list(columns=columns, filter=filter, group=group,
140 sort=sort, filterspec=filterspec)
142 # XXX deviates from spec - loses the '+' (that's a reserved character
143 # in URLS
144 def list(self, sort=None, group=None, filter=None, columns=None,
145 filterspec=None):
146 ''' call the template index with the args
148 :sort - sort by prop name, optionally preceeded with '-'
149 to give descending or nothing for ascending sorting.
150 :group - group by prop name, optionally preceeded with '-' or
151 to sort in descending or nothing for ascending order.
152 :filter - selects which props should be displayed in the filter
153 section. Default is all.
154 :columns - selects the columns that should be displayed.
155 Default is all.
157 '''
158 cn = self.classname
159 self.pagehead('Index of %s'%cn)
160 if sort is None: sort = self.index_arg(':sort')
161 if group is None: group = self.index_arg(':group')
162 if filter is None: filter = self.index_arg(':filter')
163 if columns is None: columns = self.index_arg(':columns')
164 if filterspec is None: filterspec = self.index_filterspec()
166 htmltemplate.index(self, self.TEMPLATES, self.db, cn, filterspec,
167 filter, columns, sort, group)
168 self.pagefoot()
170 def shownode(self, message=None):
171 ''' display an item
172 '''
173 cn = self.classname
174 cl = self.db.classes[cn]
176 # possibly perform an edit
177 keys = self.form.keys()
178 num_re = re.compile('^\d+$')
179 if keys:
180 changed = []
181 props = {}
182 try:
183 keys = self.form.keys()
184 for key in keys:
185 if not cl.properties.has_key(key):
186 continue
187 proptype = cl.properties[key]
188 if proptype.isStringType:
189 value = str(self.form[key].value).strip()
190 elif proptype.isDateType:
191 value = date.Date(str(self.form[key].value))
192 elif proptype.isIntervalType:
193 value = date.Interval(str(self.form[key].value))
194 elif proptype.isLinkType:
195 value = str(self.form[key].value).strip()
196 # handle key values
197 link = cl.properties[key].classname
198 if not num_re.match(value):
199 try:
200 value = self.db.classes[link].lookup(value)
201 except:
202 raise ValueError, 'property "%s": %s not a %s'%(
203 key, value, link)
204 elif proptype.isMultilinkType:
205 value = self.form[key]
206 if type(value) != type([]):
207 value = [i.strip() for i in str(value.value).split(',')]
208 else:
209 value = [str(i.value).strip() for i in value]
210 link = cl.properties[key].classname
211 l = []
212 for entry in map(str, value):
213 if not num_re.match(entry):
214 try:
215 entry = self.db.classes[link].lookup(entry)
216 except:
217 raise ValueError, \
218 'property "%s": %s not a %s'%(key,
219 entry, link)
220 l.append(entry)
221 l.sort()
222 value = l
223 # if changed, set it
224 if value != cl.get(self.nodeid, key):
225 changed.append(key)
226 props[key] = value
227 cl.set(self.nodeid, **props)
229 # if this item has messages, generate an edit message
230 # TODO: don't send the edit message to the person who
231 # performed the edit
232 if (cl.getprops().has_key('messages') and
233 cl.getprops()['messages'].isMultilinkType and
234 cl.getprops()['messages'].classname == 'msg'):
235 nid = self.nodeid
236 m = []
237 for name, prop in cl.getprops().items():
238 # TODO: the None default is only here because we
239 # don't have schema migration :(
240 if prop.isMultilinkType:
241 value = cl.get(nid, name, [])
242 else:
243 value = cl.get(nid, name, None)
244 if prop.isLinkType:
245 link = self.db.classes[prop.classname]
246 key = link.getkey()
247 if value is not None and key:
248 value = link.get(value, key)
249 else:
250 value = '-'
251 elif prop.isMultilinkType:
252 l = []
253 link = self.db.classes[prop.classname]
254 for entry in value:
255 key = link.getkey()
256 if key:
257 l.append(link.get(entry, link.getkey()))
258 else:
259 l.append(entry)
260 value = ', '.join(l)
261 if name in changed:
262 chg = '*'
263 else:
264 chg = ' '
265 m.append('%s %s: %s'%(chg, name, value))
267 # handle the note
268 if self.form.has_key('__note'):
269 note = self.form['__note'].value
270 if '\n' in note:
271 summary = re.split(r'\n\r?', note)[0]
272 else:
273 summary = note
274 m.insert(0, '%s\n\n'%note)
275 else:
276 if len(changed) > 1:
277 plural = 's were'
278 else:
279 plural = ' was'
280 summary = 'This %s has been edited through the web '\
281 'and the %s value%s changed.'%(cn,
282 ', '.join(changed), plural)
283 m.insert(0, '%s\n\n'%summary)
285 # now create the message
286 content = '\n'.join(m)
287 message_id = self.db.msg.create(author='1', recipients=[],
288 date=date.Date('.'), summary=summary, content=content)
289 messages = cl.get(nid, 'messages')
290 messages.append(message_id)
291 props = {'messages': messages}
292 cl.set(nid, **props)
294 # and some nice feedback for the user
295 message = '%s edited ok'%', '.join(changed)
296 except:
297 s = StringIO.StringIO()
298 traceback.print_exc(None, s)
299 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
301 # now the display
302 id = self.nodeid
303 if cl.getkey():
304 id = cl.get(id, cl.getkey())
305 self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
307 nodeid = self.nodeid
309 # use the template to display the item
310 htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid)
311 self.pagefoot()
312 showissue = shownode
313 showmsg = shownode
315 def newnode(self, message=None):
316 ''' Add a new node to the database.
318 The form works in two modes: blank form and submission (that is,
319 the submission goes to the same URL). **Eventually this means that
320 the form will have previously entered information in it if
321 submission fails.
323 The new node will be created with the properties specified in the
324 form submission. For multilinks, multiple form entries are handled,
325 as are prop=value,value,value. You can't mix them though.
327 If the new node is to be referenced from somewhere else immediately
328 (ie. the new node is a file that is to be attached to a support
329 issue) then supply one of these arguments in addition to the usual
330 form entries:
331 :link=designator:property
332 :multilink=designator:property
333 ... which means that once the new node is created, the "property"
334 on the node given by "designator" should now reference the new
335 node's id. The node id will be appended to the multilink.
336 '''
337 cn = self.classname
338 cl = self.db.classes[cn]
340 # possibly perform a create
341 keys = self.form.keys()
342 num_re = re.compile('^\d+$')
343 if [i for i in keys if i[0] != ':']:
344 props = {}
345 try:
346 keys = self.form.keys()
347 for key in keys:
348 if not cl.properties.has_key(key):
349 continue
350 proptype = cl.properties[key]
351 if proptype.isStringType:
352 value = self.form[key].value.strip()
353 elif proptype.isDateType:
354 value = date.Date(self.form[key].value.strip())
355 elif proptype.isIntervalType:
356 value = date.Interval(self.form[key].value.strip())
357 elif proptype.isLinkType:
358 value = self.form[key].value.strip()
359 # handle key values
360 link = cl.properties[key].classname
361 if not num_re.match(value):
362 try:
363 value = self.db.classes[link].lookup(value)
364 except:
365 raise ValueError, 'property "%s": %s not a %s'%(
366 key, value, link)
367 elif proptype.isMultilinkType:
368 value = self.form[key]
369 if type(value) != type([]):
370 value = [i.strip() for i in value.value.split(',')]
371 else:
372 value = [i.value.strip() for i in value]
373 link = cl.properties[key].classname
374 l = []
375 for entry in map(str, value):
376 if not num_re.match(entry):
377 try:
378 entry = self.db.classes[link].lookup(entry)
379 except:
380 raise ValueError, \
381 'property "%s": %s not a %s'%(key,
382 entry, link)
383 l.append(entry)
384 l.sort()
385 value = l
386 props[key] = value
387 nid = cl.create(**props)
389 # link if necessary
390 for key in keys:
391 print key,
392 if key == ':multilink':
393 value = self.form[key].value
394 if type(value) != type([]): value = [value]
395 for value in value:
396 designator, property = value.split(':')
397 print 'miltilinking to ', designator, property
398 link, nodeid = roundupdb.splitDesignator(designator)
399 link = self.db.classes[link]
400 value = link.get(nodeid, property)
401 value.append(nid)
402 link.set(nodeid, **{property: value})
403 elif key == ':link':
404 value = self.form[key].value
405 if type(value) != type([]): value = [value]
406 for value in value:
407 designator, property = value.split(':')
408 print 'linking to ', designator, property
409 link, nodeid = roundupdb.splitDesignator(designator)
410 link = self.db.classes[link]
411 link.set(nodeid, **{property: nid})
412 else:
413 print 'ignoring'
415 # if this item has messages,
416 if (cl.getprops().has_key('messages') and
417 cl.getprops()['messages'].isMultilinkType and
418 cl.getprops()['messages'].classname == 'msg'):
419 # generate an edit message - nosyreactor will send it
420 m = []
421 for name, prop in cl.getprops().items():
422 value = cl.get(nid, name)
423 if prop.isLinkType:
424 link = self.db.classes[prop.classname]
425 key = link.getkey()
426 if value is not None and key:
427 value = link.get(value, key)
428 else:
429 value = '-'
430 elif prop.isMultilinkType:
431 l = []
432 link = self.db.classes[prop.classname]
433 for entry in value:
434 key = link.getkey()
435 if key:
436 l.append(link.get(entry, link.getkey()))
437 else:
438 l.append(entry)
439 value = ', '.join(l)
440 m.append('%s: %s'%(name, value))
442 # handle the note
443 note = None
444 if self.form.has_key('__note'):
445 note = self.form['__note']
446 if note and note.value:
447 note = note.value
448 if '\n' in note:
449 summary = re.split(r'\n\r?', note)[0]
450 else:
451 summary = note
452 m.append('\n%s\n'%note)
453 else:
454 summary = 'This %s has been created through the web.'%cn
455 m.append('\n%s\s'%summary)
457 # now create the message
458 content = '\n'.join(m)
459 message_id = self.db.msg.create(author='1', recipients=[],
460 date=date.Date('.'), summary=summary, content=content)
461 messages = cl.get(nid, 'messages')
462 messages.append(message_id)
463 props = {'messages': messages}
464 cl.set(nid, **props)
466 # and some nice feedback for the user
467 message = '%s created ok'%cn
468 except:
469 s = StringIO.StringIO()
470 traceback.print_exc(None, s)
471 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
472 self.pagehead('New %s'%self.classname.capitalize(), message)
473 htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
474 self.form)
475 self.pagefoot()
476 newissue = newnode
477 newuser = newnode
479 def showuser(self, message=None):
480 ''' display an item
481 '''
482 if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
483 self.shownode(message)
484 else:
485 raise Unauthorised
487 def showfile(self):
488 ''' display a file
489 '''
490 nodeid = self.nodeid
491 cl = self.db.file
492 type = cl.get(nodeid, 'type')
493 if type == 'message/rfc822':
494 type = 'text/plain'
495 self.header(headers={'Content-Type': type})
496 self.write(cl.get(nodeid, 'content'))
498 def classes(self, message=None):
499 ''' display a list of all the classes in the database
500 '''
501 if self.user == 'admin':
502 self.pagehead('Table of classes', message)
503 classnames = self.db.classes.keys()
504 classnames.sort()
505 self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
506 for cn in classnames:
507 cl = self.db.getclass(cn)
508 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
509 for key, value in cl.properties.items():
510 if value is None: value = ''
511 else: value = str(value)
512 self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
513 key, cgi.escape(value)))
514 self.write('</table>')
515 self.pagefoot()
516 else:
517 raise Unauthorised
519 def main(self, dre=re.compile(r'([^\d]+)(\d+)'), nre=re.compile(r'new(\w+)')):
520 path = self.split_path
521 if not path or path[0] in ('', 'index'):
522 self.index()
523 elif len(path) == 1:
524 if path[0] == 'list_classes':
525 self.classes()
526 return
527 m = dre.match(path[0])
528 if m:
529 self.classname = m.group(1)
530 self.nodeid = m.group(2)
531 getattr(self, 'show%s'%self.classname)()
532 return
533 m = nre.match(path[0])
534 if m:
535 self.classname = m.group(1)
536 getattr(self, 'new%s'%self.classname)()
537 return
538 self.classname = path[0]
539 self.list()
540 else:
541 raise 'ValueError', 'Path not understood'
543 def __del__(self):
544 self.db.close()
546 #
547 # $Log: not supported by cvs2svn $
548 # Revision 1.11 2001/07/30 06:17:45 richard
549 # Features:
550 # . Added ability for cgi newblah forms to indicate that the new node
551 # should be linked somewhere.
552 # Fixed:
553 # . Fixed the agument handling for the roundup-admin find command.
554 # . Fixed handling of summary when no note supplied for newblah. Again.
555 # . Fixed detection of no form in htmltemplate Field display.
556 #
557 # Revision 1.10 2001/07/30 02:37:34 richard
558 # Temporary measure until we have decent schema migration...
559 #
560 # Revision 1.9 2001/07/30 01:25:07 richard
561 # Default implementation is now "classic" rather than "extended" as one would
562 # expect.
563 #
564 # Revision 1.8 2001/07/29 08:27:40 richard
565 # Fixed handling of passed-in values in form elements (ie. during a
566 # drill-down)
567 #
568 # Revision 1.7 2001/07/29 07:01:39 richard
569 # Added vim command to all source so that we don't get no steenkin' tabs :)
570 #
571 # Revision 1.6 2001/07/29 04:04:00 richard
572 # Moved some code around allowing for subclassing to change behaviour.
573 #
574 # Revision 1.5 2001/07/28 08:16:52 richard
575 # New issue form handles lack of note better now.
576 #
577 # Revision 1.4 2001/07/28 00:34:34 richard
578 # Fixed some non-string node ids.
579 #
580 # Revision 1.3 2001/07/23 03:56:30 richard
581 # oops, missed a config removal
582 #
583 # Revision 1.2 2001/07/22 12:09:32 richard
584 # Final commit of Grande Splite
585 #
586 # Revision 1.1 2001/07/22 11:58:35 richard
587 # More Grande Splite
588 #
589 #
590 # vim: set filetype=python ts=4 sw=4 et si