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