1 # $Id: cgi_client.py,v 1.14 2001-08-02 00:26:16 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 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 self._post_editnode(self.nodeid)
230 # and some nice feedback for the user
231 message = '%s edited ok'%', '.join(changed)
232 except:
233 s = StringIO.StringIO()
234 traceback.print_exc(None, s)
235 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
237 # now the display
238 id = self.nodeid
239 if cl.getkey():
240 id = cl.get(id, cl.getkey())
241 self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
243 nodeid = self.nodeid
245 # use the template to display the item
246 htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid)
247 self.pagefoot()
248 showissue = shownode
249 showmsg = shownode
251 def showuser(self, message=None):
252 ''' display an item
253 '''
254 if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
255 self.shownode(message)
256 else:
257 raise Unauthorised
259 def showfile(self):
260 ''' display a file
261 '''
262 nodeid = self.nodeid
263 cl = self.db.file
264 type = cl.get(nodeid, 'type')
265 if type == 'message/rfc822':
266 type = 'text/plain'
267 self.header(headers={'Content-Type': type})
268 self.write(cl.get(nodeid, 'content'))
270 def _createnode(self):
271 ''' create a node based on the contents of the form
272 '''
273 cn = self.classname
274 cl = self.db.classes[cn]
275 props = {}
276 keys = self.form.keys()
277 num_re = re.compile('^\d+$')
278 for key in keys:
279 if not cl.properties.has_key(key):
280 continue
281 proptype = cl.properties[key]
282 if proptype.isStringType:
283 value = self.form[key].value.strip()
284 elif proptype.isDateType:
285 value = date.Date(self.form[key].value.strip())
286 elif proptype.isIntervalType:
287 value = date.Interval(self.form[key].value.strip())
288 elif proptype.isLinkType:
289 value = self.form[key].value.strip()
290 # handle key values
291 link = cl.properties[key].classname
292 if not num_re.match(value):
293 try:
294 value = self.db.classes[link].lookup(value)
295 except:
296 raise ValueError, 'property "%s": %s not a %s'%(
297 key, value, link)
298 elif proptype.isMultilinkType:
299 value = self.form[key]
300 if type(value) != type([]):
301 value = [i.strip() for i in value.value.split(',')]
302 else:
303 value = [i.value.strip() for i in value]
304 link = cl.properties[key].classname
305 l = []
306 for entry in map(str, value):
307 if not num_re.match(entry):
308 try:
309 entry = self.db.classes[link].lookup(entry)
310 except:
311 raise ValueError, \
312 'property "%s": %s not a %s'%(key,
313 entry, link)
314 l.append(entry)
315 l.sort()
316 value = l
317 props[key] = value
318 return cl.create(**props)
320 def _post_editnode(self, nid):
321 ''' do the linking and message sending part of the node creation
322 '''
323 cn = self.classname
324 cl = self.db.classes[cn]
325 # link if necessary
326 keys = self.form.keys()
327 for key in keys:
328 if key == ':multilink':
329 value = self.form[key].value
330 if type(value) != type([]): value = [value]
331 for value in value:
332 designator, property = value.split(':')
333 link, nodeid = roundupdb.splitDesignator(designator)
334 link = self.db.classes[link]
335 value = link.get(nodeid, property)
336 value.append(nid)
337 link.set(nodeid, **{property: value})
338 elif key == ':link':
339 value = self.form[key].value
340 if type(value) != type([]): value = [value]
341 for value in value:
342 designator, property = value.split(':')
343 link, nodeid = roundupdb.splitDesignator(designator)
344 link = self.db.classes[link]
345 link.set(nodeid, **{property: nid})
347 # if this item has messages,
348 if (cl.getprops().has_key('messages') and
349 cl.getprops()['messages'].isMultilinkType and
350 cl.getprops()['messages'].classname == 'msg'):
351 # handle the note
352 note = None
353 if self.form.has_key('__note'):
354 note = self.form['__note']
355 if note is not None and note.value:
356 note = note.value
357 if '\n' in note:
358 summary = re.split(r'\n\r?', note)[0]
359 else:
360 summary = note
361 m = ['%s\n'%note)
362 else:
363 summary = 'This %s has been created through the web.\n'%cn
364 m = [summary]
365 m.append('\n-------\n')
367 # generate an edit message - nosyreactor will send it
368 for name, prop in cl.getprops().items():
369 value = cl.get(nid, name)
370 if prop.isLinkType:
371 link = self.db.classes[prop.classname]
372 key = link.getkey()
373 if value is not None and key:
374 value = link.get(value, key)
375 else:
376 value = '-'
377 elif prop.isMultilinkType:
378 l = []
379 link = self.db.classes[prop.classname]
380 for entry in value:
381 key = link.getkey()
382 if key:
383 l.append(link.get(entry, link.getkey()))
384 else:
385 l.append(entry)
386 value = ', '.join(l)
387 m.append('%s: %s'%(name, value))
389 # now create the message
390 content = '\n'.join(m)
391 message_id = self.db.msg.create(author='1', recipients=[],
392 date=date.Date('.'), summary=summary, content=content)
393 messages = cl.get(nid, 'messages')
394 messages.append(message_id)
395 props = {'messages': messages}
396 cl.set(nid, **props)
398 def newnode(self, message=None):
399 ''' Add a new node to the database.
401 The form works in two modes: blank form and submission (that is,
402 the submission goes to the same URL). **Eventually this means that
403 the form will have previously entered information in it if
404 submission fails.
406 The new node will be created with the properties specified in the
407 form submission. For multilinks, multiple form entries are handled,
408 as are prop=value,value,value. You can't mix them though.
410 If the new node is to be referenced from somewhere else immediately
411 (ie. the new node is a file that is to be attached to a support
412 issue) then supply one of these arguments in addition to the usual
413 form entries:
414 :link=designator:property
415 :multilink=designator:property
416 ... which means that once the new node is created, the "property"
417 on the node given by "designator" should now reference the new
418 node's id. The node id will be appended to the multilink.
419 '''
420 cn = self.classname
421 cl = self.db.classes[cn]
423 # possibly perform a create
424 keys = self.form.keys()
425 if [i for i in keys if i[0] != ':']:
426 props = {}
427 try:
428 nid = self._createnode()
429 self._post_editnode(nid)
430 # and some nice feedback for the user
431 message = '%s created ok'%cn
432 except:
433 s = StringIO.StringIO()
434 traceback.print_exc(None, s)
435 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
436 self.pagehead('New %s'%self.classname.capitalize(), message)
437 htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
438 self.form)
439 self.pagefoot()
440 newissue = newnode
441 newuser = newnode
443 def newfile(self, message=None):
444 ''' Add a new file to the database.
446 This form works very much the same way as newnode - it just has a
447 file upload.
448 '''
449 cn = self.classname
450 cl = self.db.classes[cn]
452 # possibly perform a create
453 keys = self.form.keys()
454 if [i for i in keys if i[0] != ':']:
455 try:
456 file = self.form['content']
457 self._post_editnode(cl.create(content=file.file.read(),
458 type=mimetypes.guess_type(file.filename)[0],
459 name=file.filename))
460 # and some nice feedback for the user
461 message = '%s created ok'%cn
462 except:
463 s = StringIO.StringIO()
464 traceback.print_exc(None, s)
465 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
467 self.pagehead('New %s'%self.classname.capitalize(), message)
468 htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
469 self.form)
470 self.pagefoot()
472 def classes(self, message=None):
473 ''' display a list of all the classes in the database
474 '''
475 if self.user == 'admin':
476 self.pagehead('Table of classes', message)
477 classnames = self.db.classes.keys()
478 classnames.sort()
479 self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
480 for cn in classnames:
481 cl = self.db.getclass(cn)
482 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
483 for key, value in cl.properties.items():
484 if value is None: value = ''
485 else: value = str(value)
486 self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
487 key, cgi.escape(value)))
488 self.write('</table>')
489 self.pagefoot()
490 else:
491 raise Unauthorised
493 def main(self, dre=re.compile(r'([^\d]+)(\d+)'), nre=re.compile(r'new(\w+)')):
494 path = self.split_path
495 if not path or path[0] in ('', 'index'):
496 self.index()
497 elif len(path) == 1:
498 if path[0] == 'list_classes':
499 self.classes()
500 return
501 m = dre.match(path[0])
502 if m:
503 self.classname = m.group(1)
504 self.nodeid = m.group(2)
505 getattr(self, 'show%s'%self.classname)()
506 return
507 m = nre.match(path[0])
508 if m:
509 self.classname = m.group(1)
510 getattr(self, 'new%s'%self.classname)()
511 return
512 self.classname = path[0]
513 self.list()
514 else:
515 raise 'ValueError', 'Path not understood'
517 def __del__(self):
518 self.db.close()
520 #
521 # $Log: not supported by cvs2svn $
522 # Revision 1.13 2001/07/30 08:12:17 richard
523 # Added time logging and file uploading to the templates.
524 #
525 # Revision 1.12 2001/07/30 06:26:31 richard
526 # Added some documentation on how the newblah works.
527 #
528 # Revision 1.11 2001/07/30 06:17:45 richard
529 # Features:
530 # . Added ability for cgi newblah forms to indicate that the new node
531 # should be linked somewhere.
532 # Fixed:
533 # . Fixed the agument handling for the roundup-admin find command.
534 # . Fixed handling of summary when no note supplied for newblah. Again.
535 # . Fixed detection of no form in htmltemplate Field display.
536 #
537 # Revision 1.10 2001/07/30 02:37:34 richard
538 # Temporary measure until we have decent schema migration...
539 #
540 # Revision 1.9 2001/07/30 01:25:07 richard
541 # Default implementation is now "classic" rather than "extended" as one would
542 # expect.
543 #
544 # Revision 1.8 2001/07/29 08:27:40 richard
545 # Fixed handling of passed-in values in form elements (ie. during a
546 # drill-down)
547 #
548 # Revision 1.7 2001/07/29 07:01:39 richard
549 # Added vim command to all source so that we don't get no steenkin' tabs :)
550 #
551 # Revision 1.6 2001/07/29 04:04:00 richard
552 # Moved some code around allowing for subclassing to change behaviour.
553 #
554 # Revision 1.5 2001/07/28 08:16:52 richard
555 # New issue form handles lack of note better now.
556 #
557 # Revision 1.4 2001/07/28 00:34:34 richard
558 # Fixed some non-string node ids.
559 #
560 # Revision 1.3 2001/07/23 03:56:30 richard
561 # oops, missed a config removal
562 #
563 # Revision 1.2 2001/07/22 12:09:32 richard
564 # Final commit of Grande Splite
565 #
566 # Revision 1.1 2001/07/22 11:58:35 richard
567 # More Grande Splite
568 #
569 #
570 # vim: set filetype=python ts=4 sw=4 et si