9dbed7f2b1255f22e2ccbb327aeb9157494dfc82
1 # $Id: cgi_client.py,v 1.4 2001-07-28 00:34: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 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 if self.form.has_key('__note'):
393 note = self.form['__note'].value
394 if '\n' in note:
395 summary = re.split(r'\n\r?', note)[0]
396 else:
397 summary = note
398 m.append('\n%s\n'%note)
399 else:
400 m.append('\nThis %s has been created through '
401 'the web.\n'%cn)
403 # now create the message
404 content = '\n'.join(m)
405 message_id = self.db.msg.create(author='1', recipients=[],
406 date=date.Date('.'), summary=summary, content=content)
407 messages = cl.get(nid, 'messages')
408 messages.append(message_id)
409 props = {'messages': messages}
410 cl.set(nid, **props)
412 # and some nice feedback for the user
413 message = '%s created ok'%cn
414 except:
415 s = StringIO.StringIO()
416 traceback.print_exc(None, s)
417 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
418 self.pagehead('New %s'%self.classname.capitalize(), message)
419 htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
420 self.form)
421 self.pagefoot()
423 def showuser(self, message=None):
424 ''' display an item
425 '''
426 if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
427 self.showitem(message)
428 else:
429 raise Unauthorised
431 def showfile(self):
432 ''' display a file
433 '''
434 nodeid = self.nodeid
435 cl = self.db.file
436 type = cl.get(nodeid, 'type')
437 if type == 'message/rfc822':
438 type = 'text/plain'
439 self.header(headers={'Content-Type': type})
440 self.write(cl.get(nodeid, 'content'))
442 def classes(self, message=None):
443 ''' display a list of all the classes in the database
444 '''
445 if self.user == 'admin':
446 self.pagehead('Table of classes', message)
447 classnames = self.db.classes.keys()
448 classnames.sort()
449 self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
450 for cn in classnames:
451 cl = self.db.getclass(cn)
452 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
453 for key, value in cl.properties.items():
454 if value is None: value = ''
455 else: value = str(value)
456 self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
457 key, cgi.escape(value)))
458 self.write('</table>')
459 self.pagefoot()
460 else:
461 raise Unauthorised
463 def main(self, dre=re.compile(r'([^\d]+)(\d+)'), nre=re.compile(r'new(\w+)')):
464 path = self.split_path
465 if not path or path[0] in ('', 'index'):
466 self.index()
467 elif len(path) == 1:
468 if path[0] == 'list_classes':
469 self.classes()
470 return
471 m = dre.match(path[0])
472 if m:
473 self.classname = m.group(1)
474 self.nodeid = m.group(2)
475 getattr(self, 'show%s'%self.classname)()
476 return
477 m = nre.match(path[0])
478 if m:
479 self.classname = m.group(1)
480 getattr(self, 'new%s'%self.classname)()
481 return
482 self.classname = path[0]
483 self.list()
484 else:
485 raise 'ValueError', 'Path not understood'
487 def __del__(self):
488 self.db.close()
490 #
491 # $Log: not supported by cvs2svn $
492 # Revision 1.3 2001/07/23 03:56:30 richard
493 # oops, missed a config removal
494 #
495 # Revision 1.2 2001/07/22 12:09:32 richard
496 # Final commit of Grande Splite
497 #
498 # Revision 1.1 2001/07/22 11:58:35 richard
499 # More Grande Splite
500 #