9e635244ad020cbee3cb144cda28572b89427e18
1 # $Id: cgi_client.py,v 1.5 2001-07-28 08:16:52 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 if self.user == 'admin':
45 extras = ' | <a href="list_classes">Class List</a>'
46 else:
47 extras = ''
48 self.write('''<html><head>
49 <title>%s</title>
50 <style type="text/css">%s</style>
51 </head>
52 <body bgcolor=#ffffff>
53 %s
54 <table width=100%% border=0 cellspacing=0 cellpadding=2>
55 <tr class="location-bar"><td><big><strong>%s</strong></big></td>
56 <td align=right valign=bottom>%s</td></tr>
57 <tr class="location-bar">
58 <td align=left><a href="issue?status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=activity,status,title&:group=priority">All issues</a> |
59 <a href="issue?priority=fatal-bug,bug">Bugs</a> |
60 <a href="issue?priority=usability">Support</a> |
61 <a href="issue?priority=feature">Wishlist</a> |
62 <a href="newissue">New Issue</a>
63 %s</td>
64 <td align=right><a href="user%s">Your Details</a></td>
65 </table>
66 '''%(title, style, message, title, self.user, extras, userid))
68 def pagefoot(self):
69 if self.debug:
70 self.write('<hr><small><dl>')
71 self.write('<dt><b>Path</b></dt>')
72 self.write('<dd>%s</dd>'%(', '.join(map(repr, self.split_path))))
73 keys = self.form.keys()
74 keys.sort()
75 if keys:
76 self.write('<dt><b>Form entries</b></dt>')
77 for k in self.form.keys():
78 v = str(self.form[k].value)
79 self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
80 keys = self.env.keys()
81 keys.sort()
82 self.write('<dt><b>CGI environment</b></dt>')
83 for k in keys:
84 v = self.env[k]
85 self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
86 self.write('</dl></small>')
87 self.write('</body></html>')
89 def write(self, content):
90 if not self.headers_done:
91 self.header()
92 self.out.write(content)
94 def index_arg(self, arg):
95 ''' handle the args to index - they might be a list from the form
96 (ie. submitted from a form) or they might be a command-separated
97 single string (ie. manually constructed GET args)
98 '''
99 if self.form.has_key(arg):
100 arg = self.form[arg]
101 if type(arg) == type([]):
102 return [arg.value for arg in arg]
103 return arg.value.split(',')
104 return []
106 def index_filterspec(self):
107 ''' pull the index filter spec from the form
108 '''
109 # all the other form args are filters
110 filterspec = {}
111 for key in self.form.keys():
112 if key[0] == ':': continue
113 value = self.form[key]
114 if type(value) == type([]):
115 value = [arg.value for arg in value]
116 else:
117 value = value.value.split(',')
118 l = filterspec.get(key, [])
119 l = l + value
120 filterspec[key] = l
121 return filterspec
123 def index(self):
124 ''' put up an index
125 '''
126 self.classname = 'issue'
127 if self.form.has_key(':sort'): sort = self.index_arg(':sort')
128 else: sort=['-activity']
129 if self.form.has_key(':group'): group = self.index_arg(':group')
130 else: group=['priority']
131 if self.form.has_key(':filter'): filter = self.index_arg(':filter')
132 else: filter = []
133 if self.form.has_key(':columns'): columns = self.index_arg(':columns')
134 else: columns=['activity','status','title']
135 filterspec = self.index_filterspec()
136 if not filterspec:
137 filterspec['status'] = ['1', '2', '3', '4', '5', '6', '7']
138 return self.list(columns=columns, filter=filter, group=group,
139 sort=sort, filterspec=filterspec)
141 # XXX deviates from spec - loses the '+' (that's a reserved character
142 # in URLS
143 def list(self, sort=None, group=None, filter=None, columns=None,
144 filterspec=None):
145 ''' call the template index with the args
147 :sort - sort by prop name, optionally preceeded with '-'
148 to give descending or nothing for ascending sorting.
149 :group - group by prop name, optionally preceeded with '-' or
150 to sort in descending or nothing for ascending order.
151 :filter - selects which props should be displayed in the filter
152 section. Default is all.
153 :columns - selects the columns that should be displayed.
154 Default is all.
156 '''
157 cn = self.classname
158 self.pagehead('Index: %s'%cn)
159 if sort is None: sort = self.index_arg(':sort')
160 if group is None: group = self.index_arg(':group')
161 if filter is None: filter = self.index_arg(':filter')
162 if columns is None: columns = self.index_arg(':columns')
163 if filterspec is None: filterspec = self.index_filterspec()
165 htmltemplate.index(self, self.TEMPLATES, self.db, cn, filterspec,
166 filter, columns, sort, group)
167 self.pagefoot()
169 def showitem(self, message=None):
170 ''' display an item
171 '''
172 cn = self.classname
173 cl = self.db.classes[cn]
175 # possibly perform an edit
176 keys = self.form.keys()
177 num_re = re.compile('^\d+$')
178 if keys:
179 changed = []
180 props = {}
181 try:
182 keys = self.form.keys()
183 for key in keys:
184 if not cl.properties.has_key(key):
185 continue
186 proptype = cl.properties[key]
187 if proptype.isStringType:
188 value = str(self.form[key].value).strip()
189 elif proptype.isDateType:
190 value = date.Date(str(self.form[key].value))
191 elif proptype.isIntervalType:
192 value = date.Interval(str(self.form[key].value))
193 elif proptype.isLinkType:
194 value = str(self.form[key].value).strip()
195 # handle key values
196 link = cl.properties[key].classname
197 if not num_re.match(value):
198 try:
199 value = self.db.classes[link].lookup(value)
200 except:
201 raise ValueError, 'property "%s": %s not a %s'%(
202 key, value, link)
203 elif proptype.isMultilinkType:
204 value = self.form[key]
205 if type(value) != type([]):
206 value = [i.strip() for i in str(value.value).split(',')]
207 else:
208 value = [str(i.value).strip() for i in value]
209 link = cl.properties[key].classname
210 l = []
211 for entry in map(str, value):
212 if not num_re.match(entry):
213 try:
214 entry = self.db.classes[link].lookup(entry)
215 except:
216 raise ValueError, \
217 'property "%s": %s not a %s'%(key,
218 entry, link)
219 l.append(entry)
220 l.sort()
221 value = l
222 # if changed, set it
223 if value != cl.get(self.nodeid, key):
224 changed.append(key)
225 props[key] = value
226 cl.set(self.nodeid, **props)
228 # if this item has messages, generate an edit message
229 # TODO: don't send the edit message to the person who
230 # performed the edit
231 if (cl.getprops().has_key('messages') and
232 cl.getprops()['messages'].isMultilinkType and
233 cl.getprops()['messages'].classname == 'msg'):
234 nid = self.nodeid
235 m = []
236 for name, prop in cl.getprops().items():
237 value = cl.get(nid, name)
238 if prop.isLinkType:
239 link = self.db.classes[prop.classname]
240 key = link.getkey()
241 if value is not None and key:
242 value = link.get(value, key)
243 else:
244 value = '-'
245 elif prop.isMultilinkType:
246 l = []
247 link = self.db.classes[prop.classname]
248 for entry in value:
249 key = link.getkey()
250 if key:
251 l.append(link.get(entry, link.getkey()))
252 else:
253 l.append(entry)
254 value = ', '.join(l)
255 if name in changed:
256 chg = '*'
257 else:
258 chg = ' '
259 m.append('%s %s: %s'%(chg, name, value))
261 # handle the note
262 if self.form.has_key('__note'):
263 note = self.form['__note'].value
264 if '\n' in note:
265 summary = re.split(r'\n\r?', note)[0]
266 else:
267 summary = note
268 m.insert(0, '%s\n\n'%note)
269 else:
270 if len(changed) > 1:
271 plural = 's were'
272 else:
273 plural = ' was'
274 summary = 'This %s has been edited through the web '\
275 'and the %s value%s changed.'%(cn,
276 ', '.join(changed), plural)
277 m.insert(0, '%s\n\n'%summary)
279 # now create the message
280 content = '\n'.join(m)
281 message_id = self.db.msg.create(author='1', recipients=[],
282 date=date.Date('.'), summary=summary, content=content)
283 messages = cl.get(nid, 'messages')
284 messages.append(message_id)
285 props = {'messages': messages}
286 cl.set(nid, **props)
288 # and some nice feedback for the user
289 message = '%s edited ok'%', '.join(changed)
290 except:
291 s = StringIO.StringIO()
292 traceback.print_exc(None, s)
293 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
295 # now the display
296 id = self.nodeid
297 if cl.getkey():
298 id = cl.get(id, cl.getkey())
299 self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
301 nodeid = self.nodeid
303 # use the template to display the item
304 htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid)
305 self.pagefoot()
306 showissue = showitem
307 showmsg = showitem
309 def newissue(self, message=None):
310 ''' add an issue
311 '''
312 cn = self.classname
313 cl = self.db.classes[cn]
315 # possibly perform a create
316 keys = self.form.keys()
317 num_re = re.compile('^\d+$')
318 if keys:
319 props = {}
320 try:
321 keys = self.form.keys()
322 for key in keys:
323 if not cl.properties.has_key(key):
324 continue
325 proptype = cl.properties[key]
326 if proptype.isStringType:
327 value = self.form[key].value.strip()
328 elif proptype.isDateType:
329 value = date.Date(self.form[key].value.strip())
330 elif proptype.isIntervalType:
331 value = date.Interval(self.form[key].value.strip())
332 elif proptype.isLinkType:
333 value = self.form[key].value.strip()
334 # handle key values
335 link = cl.properties[key].classname
336 if not num_re.match(value):
337 try:
338 value = self.db.classes[link].lookup(value)
339 except:
340 raise ValueError, 'property "%s": %s not a %s'%(
341 key, value, link)
342 elif proptype.isMultilinkType:
343 value = self.form[key]
344 if type(value) != type([]):
345 value = [i.strip() for i in value.value.split(',')]
346 else:
347 value = [i.value.strip() for i in value]
348 link = cl.properties[key].classname
349 l = []
350 for entry in map(str, value):
351 if not num_re.match(entry):
352 try:
353 entry = self.db.classes[link].lookup(entry)
354 except:
355 raise ValueError, \
356 'property "%s": %s not a %s'%(key,
357 entry, link)
358 l.append(entry)
359 l.sort()
360 value = l
361 props[key] = value
362 nid = cl.create(**props)
364 # if this item has messages,
365 if (cl.getprops().has_key('messages') and
366 cl.getprops()['messages'].isMultilinkType and
367 cl.getprops()['messages'].classname == 'msg'):
368 # generate an edit message - nosyreactor will send it
369 m = []
370 for name, prop in cl.getprops().items():
371 value = cl.get(nid, name)
372 if prop.isLinkType:
373 link = self.db.classes[prop.classname]
374 key = link.getkey()
375 if value is not None and key:
376 value = link.get(value, key)
377 else:
378 value = '-'
379 elif prop.isMultilinkType:
380 l = []
381 link = self.db.classes[prop.classname]
382 for entry in value:
383 key = link.getkey()
384 if key:
385 l.append(link.get(entry, link.getkey()))
386 else:
387 l.append(entry)
388 value = ', '.join(l)
389 m.append('%s: %s'%(name, value))
391 # handle the note
392 note = self.form.get('__note', None)
393 if note and note.value:
394 note = note.value
395 if '\n' in note:
396 summary = re.split(r'\n\r?', note)[0]
397 else:
398 summary = note
399 m.append('\n%s\n'%note)
400 else:
401 m.append('\nThis %s has been created through '
402 'the web.\n'%cn)
404 # now create the message
405 content = '\n'.join(m)
406 message_id = self.db.msg.create(author='1', recipients=[],
407 date=date.Date('.'), summary=summary, content=content)
408 messages = cl.get(nid, 'messages')
409 messages.append(message_id)
410 props = {'messages': messages}
411 cl.set(nid, **props)
413 # and some nice feedback for the user
414 message = '%s created ok'%cn
415 except:
416 s = StringIO.StringIO()
417 traceback.print_exc(None, s)
418 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
419 self.pagehead('New %s'%self.classname.capitalize(), message)
420 htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
421 self.form)
422 self.pagefoot()
424 def showuser(self, message=None):
425 ''' display an item
426 '''
427 if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
428 self.showitem(message)
429 else:
430 raise Unauthorised
432 def showfile(self):
433 ''' display a file
434 '''
435 nodeid = self.nodeid
436 cl = self.db.file
437 type = cl.get(nodeid, 'type')
438 if type == 'message/rfc822':
439 type = 'text/plain'
440 self.header(headers={'Content-Type': type})
441 self.write(cl.get(nodeid, 'content'))
443 def classes(self, message=None):
444 ''' display a list of all the classes in the database
445 '''
446 if self.user == 'admin':
447 self.pagehead('Table of classes', message)
448 classnames = self.db.classes.keys()
449 classnames.sort()
450 self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
451 for cn in classnames:
452 cl = self.db.getclass(cn)
453 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
454 for key, value in cl.properties.items():
455 if value is None: value = ''
456 else: value = str(value)
457 self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
458 key, cgi.escape(value)))
459 self.write('</table>')
460 self.pagefoot()
461 else:
462 raise Unauthorised
464 def main(self, dre=re.compile(r'([^\d]+)(\d+)'), nre=re.compile(r'new(\w+)')):
465 path = self.split_path
466 if not path or path[0] in ('', 'index'):
467 self.index()
468 elif len(path) == 1:
469 if path[0] == 'list_classes':
470 self.classes()
471 return
472 m = dre.match(path[0])
473 if m:
474 self.classname = m.group(1)
475 self.nodeid = m.group(2)
476 getattr(self, 'show%s'%self.classname)()
477 return
478 m = nre.match(path[0])
479 if m:
480 self.classname = m.group(1)
481 getattr(self, 'new%s'%self.classname)()
482 return
483 self.classname = path[0]
484 self.list()
485 else:
486 raise 'ValueError', 'Path not understood'
488 def __del__(self):
489 self.db.close()
491 #
492 # $Log: not supported by cvs2svn $
493 # Revision 1.4 2001/07/28 00:34:34 richard
494 # Fixed some non-string node ids.
495 #
496 # Revision 1.3 2001/07/23 03:56:30 richard
497 # oops, missed a config removal
498 #
499 # Revision 1.2 2001/07/22 12:09:32 richard
500 # Final commit of Grande Splite
501 #
502 # Revision 1.1 2001/07/22 11:58:35 richard
503 # More Grande Splite
504 #