1 # $Id: cgi_client.py,v 1.10 2001-07-30 02:37:34 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 showitem(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 = showitem
313 showmsg = showitem
315 def newissue(self, message=None):
316 ''' add an issue
317 '''
318 cn = self.classname
319 cl = self.db.classes[cn]
321 # possibly perform a create
322 keys = self.form.keys()
323 num_re = re.compile('^\d+$')
324 if keys:
325 props = {}
326 try:
327 keys = self.form.keys()
328 for key in keys:
329 if not cl.properties.has_key(key):
330 continue
331 proptype = cl.properties[key]
332 if proptype.isStringType:
333 value = self.form[key].value.strip()
334 elif proptype.isDateType:
335 value = date.Date(self.form[key].value.strip())
336 elif proptype.isIntervalType:
337 value = date.Interval(self.form[key].value.strip())
338 elif proptype.isLinkType:
339 value = self.form[key].value.strip()
340 # handle key values
341 link = cl.properties[key].classname
342 if not num_re.match(value):
343 try:
344 value = self.db.classes[link].lookup(value)
345 except:
346 raise ValueError, 'property "%s": %s not a %s'%(
347 key, value, link)
348 elif proptype.isMultilinkType:
349 value = self.form[key]
350 if type(value) != type([]):
351 value = [i.strip() for i in value.value.split(',')]
352 else:
353 value = [i.value.strip() for i in value]
354 link = cl.properties[key].classname
355 l = []
356 for entry in map(str, value):
357 if not num_re.match(entry):
358 try:
359 entry = self.db.classes[link].lookup(entry)
360 except:
361 raise ValueError, \
362 'property "%s": %s not a %s'%(key,
363 entry, link)
364 l.append(entry)
365 l.sort()
366 value = l
367 props[key] = value
368 nid = cl.create(**props)
370 # if this item has messages,
371 if (cl.getprops().has_key('messages') and
372 cl.getprops()['messages'].isMultilinkType and
373 cl.getprops()['messages'].classname == 'msg'):
374 # generate an edit message - nosyreactor will send it
375 m = []
376 for name, prop in cl.getprops().items():
377 value = cl.get(nid, name)
378 if prop.isLinkType:
379 link = self.db.classes[prop.classname]
380 key = link.getkey()
381 if value is not None and key:
382 value = link.get(value, key)
383 else:
384 value = '-'
385 elif prop.isMultilinkType:
386 l = []
387 link = self.db.classes[prop.classname]
388 for entry in value:
389 key = link.getkey()
390 if key:
391 l.append(link.get(entry, link.getkey()))
392 else:
393 l.append(entry)
394 value = ', '.join(l)
395 m.append('%s: %s'%(name, value))
397 # handle the note
398 note = None
399 if self.form.has_key('__note'):
400 note = self.form['__note']
401 if note and note.value:
402 note = note.value
403 if '\n' in note:
404 summary = re.split(r'\n\r?', note)[0]
405 else:
406 summary = note
407 m.append('\n%s\n'%note)
408 else:
409 m.append('\nThis %s has been created through '
410 'the web.\n'%cn)
412 # now create the message
413 content = '\n'.join(m)
414 message_id = self.db.msg.create(author='1', recipients=[],
415 date=date.Date('.'), summary=summary, content=content)
416 messages = cl.get(nid, 'messages')
417 messages.append(message_id)
418 props = {'messages': messages}
419 cl.set(nid, **props)
421 # and some nice feedback for the user
422 message = '%s created ok'%cn
423 except:
424 s = StringIO.StringIO()
425 traceback.print_exc(None, s)
426 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
427 self.pagehead('New %s'%self.classname.capitalize(), message)
428 htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
429 self.form)
430 self.pagefoot()
431 newuser = newissue
433 def showuser(self, message=None):
434 ''' display an item
435 '''
436 if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
437 self.showitem(message)
438 else:
439 raise Unauthorised
441 def showfile(self):
442 ''' display a file
443 '''
444 nodeid = self.nodeid
445 cl = self.db.file
446 type = cl.get(nodeid, 'type')
447 if type == 'message/rfc822':
448 type = 'text/plain'
449 self.header(headers={'Content-Type': type})
450 self.write(cl.get(nodeid, 'content'))
452 def classes(self, message=None):
453 ''' display a list of all the classes in the database
454 '''
455 if self.user == 'admin':
456 self.pagehead('Table of classes', message)
457 classnames = self.db.classes.keys()
458 classnames.sort()
459 self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
460 for cn in classnames:
461 cl = self.db.getclass(cn)
462 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
463 for key, value in cl.properties.items():
464 if value is None: value = ''
465 else: value = str(value)
466 self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
467 key, cgi.escape(value)))
468 self.write('</table>')
469 self.pagefoot()
470 else:
471 raise Unauthorised
473 def main(self, dre=re.compile(r'([^\d]+)(\d+)'), nre=re.compile(r'new(\w+)')):
474 path = self.split_path
475 if not path or path[0] in ('', 'index'):
476 self.index()
477 elif len(path) == 1:
478 if path[0] == 'list_classes':
479 self.classes()
480 return
481 m = dre.match(path[0])
482 if m:
483 self.classname = m.group(1)
484 self.nodeid = m.group(2)
485 getattr(self, 'show%s'%self.classname)()
486 return
487 m = nre.match(path[0])
488 if m:
489 self.classname = m.group(1)
490 getattr(self, 'new%s'%self.classname)()
491 return
492 self.classname = path[0]
493 self.list()
494 else:
495 raise 'ValueError', 'Path not understood'
497 def __del__(self):
498 self.db.close()
500 #
501 # $Log: not supported by cvs2svn $
502 # Revision 1.9 2001/07/30 01:25:07 richard
503 # Default implementation is now "classic" rather than "extended" as one would
504 # expect.
505 #
506 # Revision 1.8 2001/07/29 08:27:40 richard
507 # Fixed handling of passed-in values in form elements (ie. during a
508 # drill-down)
509 #
510 # Revision 1.7 2001/07/29 07:01:39 richard
511 # Added vim command to all source so that we don't get no steenkin' tabs :)
512 #
513 # Revision 1.6 2001/07/29 04:04:00 richard
514 # Moved some code around allowing for subclassing to change behaviour.
515 #
516 # Revision 1.5 2001/07/28 08:16:52 richard
517 # New issue form handles lack of note better now.
518 #
519 # Revision 1.4 2001/07/28 00:34:34 richard
520 # Fixed some non-string node ids.
521 #
522 # Revision 1.3 2001/07/23 03:56:30 richard
523 # oops, missed a config removal
524 #
525 # Revision 1.2 2001/07/22 12:09:32 richard
526 # Final commit of Grande Splite
527 #
528 # Revision 1.1 2001/07/22 11:58:35 richard
529 # More Grande Splite
530 #
531 #
532 # vim: set filetype=python ts=4 sw=4 et si