Code

Fixed handling of passed-in values in form elements (ie. during a
[roundup.git] / roundup / cgi_client.py
1 # $Id: cgi_client.py,v 1.8 2001-07-29 08:27:40 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
109         Links and multilinks want to be lists - the rest are straight
110         strings.
111         '''
112         props = self.db.classes[self.classname].getprops()
113         # all the form args not starting with ':' are filters
114         filterspec = {}
115         for key in self.form.keys():
116             if key[0] == ':': continue
117             prop = props[key]
118             value = self.form[key]
119             if prop.isLinkType or prop.isMultilinkType:
120                 if type(value) == type([]):
121                     value = [arg.value for arg in value]
122                 else:
123                     value = value.value.split(',')
124                 l = filterspec.get(key, [])
125                 l = l + value
126                 filterspec[key] = l
127             else:
128                 filterspec[key] = value.value
129         return filterspec
131     default_index_sort = ['-activity']
132     default_index_group = ['priority']
133     default_index_filter = []
134     default_index_columns = ['activity','status','title']
135     default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
136     def index(self):
137         ''' put up an index
138         '''
139         self.classname = 'issue'
140         if self.form.has_key(':sort'): sort = self.index_arg(':sort')
141         else: sort = self.default_index_sort
142         if self.form.has_key(':group'): group = self.index_arg(':group')
143         else: group = self.default_index_group
144         if self.form.has_key(':filter'): filter = self.index_arg(':filter')
145         else: filter = self.default_index_filter
146         if self.form.has_key(':columns'): columns = self.index_arg(':columns')
147         else: columns = self.default_index_columns
148         filterspec = self.index_filterspec()
149         if not filterspec:
150             filterspec = self.default_index_filterspec
151         return self.list(columns=columns, filter=filter, group=group,
152             sort=sort, filterspec=filterspec)
154     # XXX deviates from spec - loses the '+' (that's a reserved character
155     # in URLS
156     def list(self, sort=None, group=None, filter=None, columns=None,
157             filterspec=None):
158         ''' call the template index with the args
160             :sort    - sort by prop name, optionally preceeded with '-'
161                      to give descending or nothing for ascending sorting.
162             :group   - group by prop name, optionally preceeded with '-' or
163                      to sort in descending or nothing for ascending order.
164             :filter  - selects which props should be displayed in the filter
165                      section. Default is all.
166             :columns - selects the columns that should be displayed.
167                      Default is all.
169         '''
170         cn = self.classname
171         self.pagehead('Index of %s'%cn)
172         if sort is None: sort = self.index_arg(':sort')
173         if group is None: group = self.index_arg(':group')
174         if filter is None: filter = self.index_arg(':filter')
175         if columns is None: columns = self.index_arg(':columns')
176         if filterspec is None: filterspec = self.index_filterspec()
178         htmltemplate.index(self, self.TEMPLATES, self.db, cn, filterspec,
179             filter, columns, sort, group)
180         self.pagefoot()
182     def showitem(self, message=None):
183         ''' display an item
184         '''
185         cn = self.classname
186         cl = self.db.classes[cn]
188         # possibly perform an edit
189         keys = self.form.keys()
190         num_re = re.compile('^\d+$')
191         if keys:
192             changed = []
193             props = {}
194             try:
195                 keys = self.form.keys()
196                 for key in keys:
197                     if not cl.properties.has_key(key):
198                         continue
199                     proptype = cl.properties[key]
200                     if proptype.isStringType:
201                         value = str(self.form[key].value).strip()
202                     elif proptype.isDateType:
203                         value = date.Date(str(self.form[key].value))
204                     elif proptype.isIntervalType:
205                         value = date.Interval(str(self.form[key].value))
206                     elif proptype.isLinkType:
207                         value = str(self.form[key].value).strip()
208                         # handle key values
209                         link = cl.properties[key].classname
210                         if not num_re.match(value):
211                             try:
212                                 value = self.db.classes[link].lookup(value)
213                             except:
214                                 raise ValueError, 'property "%s": %s not a %s'%(
215                                     key, value, link)
216                     elif proptype.isMultilinkType:
217                         value = self.form[key]
218                         if type(value) != type([]):
219                             value = [i.strip() for i in str(value.value).split(',')]
220                         else:
221                             value = [str(i.value).strip() for i in value]
222                         link = cl.properties[key].classname
223                         l = []
224                         for entry in map(str, value):
225                             if not num_re.match(entry):
226                                 try:
227                                     entry = self.db.classes[link].lookup(entry)
228                                 except:
229                                     raise ValueError, \
230                                         'property "%s": %s not a %s'%(key,
231                                         entry, link)
232                             l.append(entry)
233                         l.sort()
234                         value = l
235                     # if changed, set it
236                     if value != cl.get(self.nodeid, key):
237                         changed.append(key)
238                         props[key] = value
239                 cl.set(self.nodeid, **props)
241                 # if this item has messages, generate an edit message
242                 # TODO: don't send the edit message to the person who
243                 # performed the edit
244                 if (cl.getprops().has_key('messages') and
245                         cl.getprops()['messages'].isMultilinkType and
246                         cl.getprops()['messages'].classname == 'msg'):
247                     nid = self.nodeid
248                     m = []
249                     for name, prop in cl.getprops().items():
250                         value = cl.get(nid, name)
251                         if prop.isLinkType:
252                             link = self.db.classes[prop.classname]
253                             key = link.getkey()
254                             if value is not None and key:
255                                 value = link.get(value, key)
256                             else:
257                                 value = '-'
258                         elif prop.isMultilinkType:
259                             l = []
260                             link = self.db.classes[prop.classname]
261                             for entry in value:
262                                 key = link.getkey()
263                                 if key:
264                                     l.append(link.get(entry, link.getkey()))
265                                 else:
266                                     l.append(entry)
267                             value = ', '.join(l)
268                         if name in changed:
269                             chg = '*'
270                         else:
271                             chg = ' '
272                         m.append('%s %s: %s'%(chg, name, value))
274                     # handle the note
275                     if self.form.has_key('__note'):
276                         note = self.form['__note'].value
277                         if '\n' in note:
278                             summary = re.split(r'\n\r?', note)[0]
279                         else:
280                             summary = note
281                         m.insert(0, '%s\n\n'%note)
282                     else:
283                         if len(changed) > 1:
284                             plural = 's were'
285                         else:
286                             plural = ' was'
287                         summary = 'This %s has been edited through the web '\
288                             'and the %s value%s changed.'%(cn,
289                             ', '.join(changed), plural)
290                         m.insert(0, '%s\n\n'%summary)
292                     # now create the message
293                     content = '\n'.join(m)
294                     message_id = self.db.msg.create(author='1', recipients=[],
295                         date=date.Date('.'), summary=summary, content=content)
296                     messages = cl.get(nid, 'messages')
297                     messages.append(message_id)
298                     props = {'messages': messages}
299                     cl.set(nid, **props)
301                 # and some nice feedback for the user
302                 message = '%s edited ok'%', '.join(changed)
303             except:
304                 s = StringIO.StringIO()
305                 traceback.print_exc(None, s)
306                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
308         # now the display
309         id = self.nodeid
310         if cl.getkey():
311             id = cl.get(id, cl.getkey())
312         self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
314         nodeid = self.nodeid
316         # use the template to display the item
317         htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid)
318         self.pagefoot()
319     showissue = showitem
320     showmsg = showitem
322     def newissue(self, message=None):
323         ''' add an issue
324         '''
325         cn = self.classname
326         cl = self.db.classes[cn]
328         # possibly perform a create
329         keys = self.form.keys()
330         num_re = re.compile('^\d+$')
331         if keys:
332             props = {}
333             try:
334                 keys = self.form.keys()
335                 for key in keys:
336                     if not cl.properties.has_key(key):
337                         continue
338                     proptype = cl.properties[key]
339                     if proptype.isStringType:
340                         value = self.form[key].value.strip()
341                     elif proptype.isDateType:
342                         value = date.Date(self.form[key].value.strip())
343                     elif proptype.isIntervalType:
344                         value = date.Interval(self.form[key].value.strip())
345                     elif proptype.isLinkType:
346                         value = self.form[key].value.strip()
347                         # handle key values
348                         link = cl.properties[key].classname
349                         if not num_re.match(value):
350                             try:
351                                 value = self.db.classes[link].lookup(value)
352                             except:
353                                 raise ValueError, 'property "%s": %s not a %s'%(
354                                     key, value, link)
355                     elif proptype.isMultilinkType:
356                         value = self.form[key]
357                         if type(value) != type([]):
358                             value = [i.strip() for i in value.value.split(',')]
359                         else:
360                             value = [i.value.strip() for i in value]
361                         link = cl.properties[key].classname
362                         l = []
363                         for entry in map(str, value):
364                             if not num_re.match(entry):
365                                 try:
366                                     entry = self.db.classes[link].lookup(entry)
367                                 except:
368                                     raise ValueError, \
369                                         'property "%s": %s not a %s'%(key,
370                                         entry, link)
371                             l.append(entry)
372                         l.sort()
373                         value = l
374                     props[key] = value
375                 nid = cl.create(**props)
377                 # if this item has messages, 
378                 if (cl.getprops().has_key('messages') and
379                         cl.getprops()['messages'].isMultilinkType and
380                         cl.getprops()['messages'].classname == 'msg'):
381                     # generate an edit message - nosyreactor will send it
382                     m = []
383                     for name, prop in cl.getprops().items():
384                         value = cl.get(nid, name)
385                         if prop.isLinkType:
386                             link = self.db.classes[prop.classname]
387                             key = link.getkey()
388                             if value is not None and key:
389                                 value = link.get(value, key)
390                             else:
391                                 value = '-'
392                         elif prop.isMultilinkType:
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                     # handle the note
405                     note = self.form.get('__note', None)
406                     if note and note.value:
407                         note = note.value
408                         if '\n' in note:
409                             summary = re.split(r'\n\r?', note)[0]
410                         else:
411                             summary = note
412                         m.append('\n%s\n'%note)
413                     else:
414                         m.append('\nThis %s has been created through '
415                             'the web.\n'%cn)
417                     # now create the message
418                     content = '\n'.join(m)
419                     message_id = self.db.msg.create(author='1', recipients=[],
420                         date=date.Date('.'), summary=summary, content=content)
421                     messages = cl.get(nid, 'messages')
422                     messages.append(message_id)
423                     props = {'messages': messages}
424                     cl.set(nid, **props)
426                 # and some nice feedback for the user
427                 message = '%s created ok'%cn
428             except:
429                 s = StringIO.StringIO()
430                 traceback.print_exc(None, s)
431                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
432         self.pagehead('New %s'%self.classname.capitalize(), message)
433         htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
434             self.form)
435         self.pagefoot()
437     def showuser(self, message=None):
438         ''' display an item
439         '''
440         if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
441             self.showitem(message)
442         else:
443             raise Unauthorised
445     def showfile(self):
446         ''' display a file
447         '''
448         nodeid = self.nodeid
449         cl = self.db.file
450         type = cl.get(nodeid, 'type')
451         if type == 'message/rfc822':
452             type = 'text/plain'
453         self.header(headers={'Content-Type': type})
454         self.write(cl.get(nodeid, 'content'))
456     def classes(self, message=None):
457         ''' display a list of all the classes in the database
458         '''
459         if self.user == 'admin':
460             self.pagehead('Table of classes', message)
461             classnames = self.db.classes.keys()
462             classnames.sort()
463             self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
464             for cn in classnames:
465                 cl = self.db.getclass(cn)
466                 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
467                 for key, value in cl.properties.items():
468                     if value is None: value = ''
469                     else: value = str(value)
470                     self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
471                         key, cgi.escape(value)))
472             self.write('</table>')
473             self.pagefoot()
474         else:
475             raise Unauthorised
477     def main(self, dre=re.compile(r'([^\d]+)(\d+)'), nre=re.compile(r'new(\w+)')):
478         path = self.split_path
479         if not path or path[0] in ('', 'index'):
480             self.index()
481         elif len(path) == 1:
482             if path[0] == 'list_classes':
483                 self.classes()
484                 return
485             m = dre.match(path[0])
486             if m:
487                 self.classname = m.group(1)
488                 self.nodeid = m.group(2)
489                 getattr(self, 'show%s'%self.classname)()
490                 return
491             m = nre.match(path[0])
492             if m:
493                 self.classname = m.group(1)
494                 getattr(self, 'new%s'%self.classname)()
495                 return
496             self.classname = path[0]
497             self.list()
498         else:
499             raise 'ValueError', 'Path not understood'
501     def __del__(self):
502         self.db.close()
505 # $Log: not supported by cvs2svn $
506 # Revision 1.7  2001/07/29 07:01:39  richard
507 # Added vim command to all source so that we don't get no steenkin' tabs :)
509 # Revision 1.6  2001/07/29 04:04:00  richard
510 # Moved some code around allowing for subclassing to change behaviour.
512 # Revision 1.5  2001/07/28 08:16:52  richard
513 # New issue form handles lack of note better now.
515 # Revision 1.4  2001/07/28 00:34:34  richard
516 # Fixed some non-string node ids.
518 # Revision 1.3  2001/07/23 03:56:30  richard
519 # oops, missed a config removal
521 # Revision 1.2  2001/07/22 12:09:32  richard
522 # Final commit of Grande Splite
524 # Revision 1.1  2001/07/22 11:58:35  richard
525 # More Grande Splite
528 # vim: set filetype=python ts=4 sw=4 et si