8e750666710756a9f3e6fee49cd4c8e395a0d40b
1 # $Id: cgi_client.py,v 1.9 2001-07-30 01:25:07 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 value = cl.get(nid, name)
239 if prop.isLinkType:
240 link = self.db.classes[prop.classname]
241 key = link.getkey()
242 if value is not None and key:
243 value = link.get(value, key)
244 else:
245 value = '-'
246 elif prop.isMultilinkType:
247 l = []
248 link = self.db.classes[prop.classname]
249 for entry in value:
250 key = link.getkey()
251 if key:
252 l.append(link.get(entry, link.getkey()))
253 else:
254 l.append(entry)
255 value = ', '.join(l)
256 if name in changed:
257 chg = '*'
258 else:
259 chg = ' '
260 m.append('%s %s: %s'%(chg, name, value))
262 # handle the note
263 if self.form.has_key('__note'):
264 note = self.form['__note'].value
265 if '\n' in note:
266 summary = re.split(r'\n\r?', note)[0]
267 else:
268 summary = note
269 m.insert(0, '%s\n\n'%note)
270 else:
271 if len(changed) > 1:
272 plural = 's were'
273 else:
274 plural = ' was'
275 summary = 'This %s has been edited through the web '\
276 'and the %s value%s changed.'%(cn,
277 ', '.join(changed), plural)
278 m.insert(0, '%s\n\n'%summary)
280 # now create the message
281 content = '\n'.join(m)
282 message_id = self.db.msg.create(author='1', recipients=[],
283 date=date.Date('.'), summary=summary, content=content)
284 messages = cl.get(nid, 'messages')
285 messages.append(message_id)
286 props = {'messages': messages}
287 cl.set(nid, **props)
289 # and some nice feedback for the user
290 message = '%s edited ok'%', '.join(changed)
291 except:
292 s = StringIO.StringIO()
293 traceback.print_exc(None, s)
294 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
296 # now the display
297 id = self.nodeid
298 if cl.getkey():
299 id = cl.get(id, cl.getkey())
300 self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
302 nodeid = self.nodeid
304 # use the template to display the item
305 htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid)
306 self.pagefoot()
307 showissue = showitem
308 showmsg = showitem
310 def newissue(self, message=None):
311 ''' add an issue
312 '''
313 cn = self.classname
314 cl = self.db.classes[cn]
316 # possibly perform a create
317 keys = self.form.keys()
318 num_re = re.compile('^\d+$')
319 if keys:
320 props = {}
321 try:
322 keys = self.form.keys()
323 for key in keys:
324 if not cl.properties.has_key(key):
325 continue
326 proptype = cl.properties[key]
327 if proptype.isStringType:
328 value = self.form[key].value.strip()
329 elif proptype.isDateType:
330 value = date.Date(self.form[key].value.strip())
331 elif proptype.isIntervalType:
332 value = date.Interval(self.form[key].value.strip())
333 elif proptype.isLinkType:
334 value = self.form[key].value.strip()
335 # handle key values
336 link = cl.properties[key].classname
337 if not num_re.match(value):
338 try:
339 value = self.db.classes[link].lookup(value)
340 except:
341 raise ValueError, 'property "%s": %s not a %s'%(
342 key, value, link)
343 elif proptype.isMultilinkType:
344 value = self.form[key]
345 if type(value) != type([]):
346 value = [i.strip() for i in value.value.split(',')]
347 else:
348 value = [i.value.strip() for i in value]
349 link = cl.properties[key].classname
350 l = []
351 for entry in map(str, value):
352 if not num_re.match(entry):
353 try:
354 entry = self.db.classes[link].lookup(entry)
355 except:
356 raise ValueError, \
357 'property "%s": %s not a %s'%(key,
358 entry, link)
359 l.append(entry)
360 l.sort()
361 value = l
362 props[key] = value
363 nid = cl.create(**props)
365 # if this item has messages,
366 if (cl.getprops().has_key('messages') and
367 cl.getprops()['messages'].isMultilinkType and
368 cl.getprops()['messages'].classname == 'msg'):
369 # generate an edit message - nosyreactor will send it
370 m = []
371 for name, prop in cl.getprops().items():
372 value = cl.get(nid, name)
373 if prop.isLinkType:
374 link = self.db.classes[prop.classname]
375 key = link.getkey()
376 if value is not None and key:
377 value = link.get(value, key)
378 else:
379 value = '-'
380 elif prop.isMultilinkType:
381 l = []
382 link = self.db.classes[prop.classname]
383 for entry in value:
384 key = link.getkey()
385 if key:
386 l.append(link.get(entry, link.getkey()))
387 else:
388 l.append(entry)
389 value = ', '.join(l)
390 m.append('%s: %s'%(name, value))
392 # handle the note
393 note = None
394 if self.form.has_key('__note'):
395 note = self.form['__note']
396 if note and note.value:
397 note = note.value
398 if '\n' in note:
399 summary = re.split(r'\n\r?', note)[0]
400 else:
401 summary = note
402 m.append('\n%s\n'%note)
403 else:
404 m.append('\nThis %s has been created through '
405 'the web.\n'%cn)
407 # now create the message
408 content = '\n'.join(m)
409 message_id = self.db.msg.create(author='1', recipients=[],
410 date=date.Date('.'), summary=summary, content=content)
411 messages = cl.get(nid, 'messages')
412 messages.append(message_id)
413 props = {'messages': messages}
414 cl.set(nid, **props)
416 # and some nice feedback for the user
417 message = '%s created ok'%cn
418 except:
419 s = StringIO.StringIO()
420 traceback.print_exc(None, s)
421 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
422 self.pagehead('New %s'%self.classname.capitalize(), message)
423 htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
424 self.form)
425 self.pagefoot()
426 newuser = newissue
428 def showuser(self, message=None):
429 ''' display an item
430 '''
431 if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
432 self.showitem(message)
433 else:
434 raise Unauthorised
436 def showfile(self):
437 ''' display a file
438 '''
439 nodeid = self.nodeid
440 cl = self.db.file
441 type = cl.get(nodeid, 'type')
442 if type == 'message/rfc822':
443 type = 'text/plain'
444 self.header(headers={'Content-Type': type})
445 self.write(cl.get(nodeid, 'content'))
447 def classes(self, message=None):
448 ''' display a list of all the classes in the database
449 '''
450 if self.user == 'admin':
451 self.pagehead('Table of classes', message)
452 classnames = self.db.classes.keys()
453 classnames.sort()
454 self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
455 for cn in classnames:
456 cl = self.db.getclass(cn)
457 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
458 for key, value in cl.properties.items():
459 if value is None: value = ''
460 else: value = str(value)
461 self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
462 key, cgi.escape(value)))
463 self.write('</table>')
464 self.pagefoot()
465 else:
466 raise Unauthorised
468 def main(self, dre=re.compile(r'([^\d]+)(\d+)'), nre=re.compile(r'new(\w+)')):
469 path = self.split_path
470 if not path or path[0] in ('', 'index'):
471 self.index()
472 elif len(path) == 1:
473 if path[0] == 'list_classes':
474 self.classes()
475 return
476 m = dre.match(path[0])
477 if m:
478 self.classname = m.group(1)
479 self.nodeid = m.group(2)
480 getattr(self, 'show%s'%self.classname)()
481 return
482 m = nre.match(path[0])
483 if m:
484 self.classname = m.group(1)
485 getattr(self, 'new%s'%self.classname)()
486 return
487 self.classname = path[0]
488 self.list()
489 else:
490 raise 'ValueError', 'Path not understood'
492 def __del__(self):
493 self.db.close()
495 #
496 # $Log: not supported by cvs2svn $
497 # Revision 1.8 2001/07/29 08:27:40 richard
498 # Fixed handling of passed-in values in form elements (ie. during a
499 # drill-down)
500 #
501 # Revision 1.7 2001/07/29 07:01:39 richard
502 # Added vim command to all source so that we don't get no steenkin' tabs :)
503 #
504 # Revision 1.6 2001/07/29 04:04:00 richard
505 # Moved some code around allowing for subclassing to change behaviour.
506 #
507 # Revision 1.5 2001/07/28 08:16:52 richard
508 # New issue form handles lack of note better now.
509 #
510 # Revision 1.4 2001/07/28 00:34:34 richard
511 # Fixed some non-string node ids.
512 #
513 # Revision 1.3 2001/07/23 03:56:30 richard
514 # oops, missed a config removal
515 #
516 # Revision 1.2 2001/07/22 12:09:32 richard
517 # Final commit of Grande Splite
518 #
519 # Revision 1.1 2001/07/22 11:58:35 richard
520 # More Grande Splite
521 #
522 #
523 # vim: set filetype=python ts=4 sw=4 et si