Code

. Fixed bug in re generation in the filter (I hadn't finished the code ;)
[roundup.git] / roundup_cgi.py
1 import os, cgi, pprint, StringIO, urlparse, re, traceback
3 import config, roundupdb, template, date
5 class Unauthorised(ValueError):
6     pass
8 class Client:
9     def __init__(self, out, env, user):
10         self.out = out
11         self.headers_done = 0
12         self.env = env
13         self.path = env.get("PATH_INFO", '').strip()
14         self.user = user
15         self.form = cgi.FieldStorage(environ=env)
16         self.split_path = self.path.split('/')[1:]
17         self.db = roundupdb.openDB(config.DATABASE, self.user)
18         self.headers_done = 0
19         self.debug = 0
21     def header(self, headers={'Content-Type':'text/html'}):
22         if not headers.has_key('Content-Type'):
23             headers['Content-Type'] = 'text/html'
24         for entry in headers.items():
25             self.out.write('%s: %s\n'%entry)
26         self.out.write('\n')
27         self.headers_done = 1
29     def pagehead(self, title, message=None):
30         url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
31         machine = self.env['SERVER_NAME']
32         port = self.env['SERVER_PORT']
33         if port != '80': machine = machine + ':' + port
34         base = urlparse.urlunparse(('http', machine, url, None, None, None))
35         if message is not None:
36             message = '<div class="system-msg">%s</div>'%message
37         else:
38             message = ''
39         style = open('style.css').read()
40         userid = self.db.user.lookup(self.user)
41         if self.user == 'admin':
42             extras = ' | <a href="list_classes">Class List</a>'
43         else:
44             extras = ''
45         self.write('''<html><head>
46 <title>%s</title>
47 <style type="text/css">%s</style>
48 </head>
49 <body bgcolor=#ffffff>
50 %s
51 <table width=100%% border=0 cellspacing=0 cellpadding=2>
52 <tr class="location-bar"><td><big><strong>%s</strong></big></td>
53 <td align=right valign=bottom>%s</td></tr>
54 <tr class="location-bar">
55 <td align=left><a href="issue?:columns=activity,status,title&:group=priority">All issues</a> | 
56 <a href="issue?priority=fatal-bug,bug">Bugs</a> | 
57 <a href="issue?priority=usability">Support</a> | 
58 <a href="issue?priority=feature">Wishlist</a> | 
59 <a href="newissue">New Issue</a>
60 %s</td>
61 <td align=right><a href="user%s">Your Details</a></td>
62 </table>
63 '''%(title, style, message, title, self.user, extras, userid))
65     def pagefoot(self):
66         if self.debug:
67             self.write('<hr><small><dl>')
68             self.write('<dt><b>Path</b></dt>')
69             self.write('<dd>%s</dd>'%(', '.join(map(repr, self.split_path))))
70             keys = self.form.keys()
71             keys.sort()
72             if keys:
73                 self.write('<dt><b>Form entries</b></dt>')
74                 for k in self.form.keys():
75                     v = str(self.form[k].value)
76                     self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
77             keys = self.env.keys()
78             keys.sort()
79             self.write('<dt><b>CGI environment</b></dt>')
80             for k in keys:
81                 v = self.env[k]
82                 self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
83             self.write('</dl></small>')
84         self.write('</body></html>')
86     def write(self, content):
87         if not self.headers_done:
88             self.header()
89         self.out.write(content)
91     def index_arg(self, arg):
92         ''' handle the args to index - they might be a list from the form
93             (ie. submitted from a form) or they might be a command-separated
94             single string (ie. manually constructed GET args)
95         '''
96         if self.form.has_key(arg):
97             arg =  self.form[arg]
98             if type(arg) == type([]):
99                 return [arg.value for arg in arg]
100             return arg.value.split(',')
101         return []
103     def index(self):
104         ''' put up an index
105         '''
106         self.classname = 'issue'
107         if self.form.has_key(':sort'): sort = self.index_arg(':sort')
108         else: sort=['-activity']
109         if self.form.has_key(':group'): group = self.index_arg(':group')
110         else: group=['priority']
111         if self.form.has_key(':filter'): filter = self.index_arg(':filter')
112         else: filter = []
113         if self.form.has_key(':columns'): columns = self.index_arg(':columns')
114         else: columns=['activity','status','title']
115         return self.list(columns=columns, filter=filter, group=group, sort=sort)
117     # XXX deviates from spec - loses the '+' (that's a reserved character
118     # in URLS
119     def list(self, sort=None, group=None, filter=None, columns=None):
120         ''' call the template index with the args
122             :sort    - sort by prop name, optionally preceeded with '-'
123                      to give descending or nothing for ascending sorting.
124             :group   - group by prop name, optionally preceeded with '-' or
125                      to sort in descending or nothing for ascending order.
126             :filter  - selects which props should be displayed in the filter
127                      section. Default is all.
128             :columns - selects the columns that should be displayed.
129                      Default is all.
131         '''
132         cn = self.classname
133         self.pagehead('Index: %s'%cn)
134         if sort is None: sort = self.index_arg(':sort')
135         if group is None: group = self.index_arg(':group')
136         if filter is None: filter = self.index_arg(':filter')
137         if columns is None: columns = self.index_arg(':columns')
139         # all the other form args are filters
140         filterspec = {}
141         for key in self.form.keys():
142             if key[0] == ':': continue
143             value = self.form[key]
144             if type(value) == type([]):
145                 value = [arg.value for arg in value]
146             else:
147                 value = value.value.split(',')
148             l = filterspec.get(key, [])
149             l = l + value
150             filterspec[key] = l
152         template.index(self, self.db, cn, filterspec, filter, columns, sort,
153             group)
154         self.pagefoot()
156     def showitem(self, message=None):
157         ''' display an item
158         '''
159         cn = self.classname
160         cl = self.db.classes[cn]
162         # possibly perform an edit
163         keys = self.form.keys()
164         num_re = re.compile('^\d+$')
165         if keys:
166             changed = []
167             props = {}
168             try:
169                 keys = self.form.keys()
170                 for key in keys:
171                     if not cl.properties.has_key(key):
172                         continue
173                     proptype = cl.properties[key]
174                     if proptype.isStringType:
175                         value = str(self.form[key].value).strip()
176                     elif proptype.isDateType:
177                         value = date.Date(str(self.form[key].value))
178                     elif proptype.isIntervalType:
179                         value = date.Interval(str(self.form[key].value))
180                     elif proptype.isLinkType:
181                         value = str(self.form[key].value).strip()
182                         # handle key values
183                         link = cl.properties[key].classname
184                         if not num_re.match(value):
185                             try:
186                                 value = self.db.classes[link].lookup(value)
187                             except:
188                                 raise ValueError, 'property "%s": %s not a %s'%(
189                                     key, value, link)
190                     elif proptype.isMultilinkType:
191                         value = self.form[key]
192                         if type(value) != type([]):
193                             value = [i.strip() for i in str(value.value).split(',')]
194                         else:
195                             value = [str(i.value).strip() for i in value]
196                         link = cl.properties[key].classname
197                         l = []
198                         for entry in map(str, value):
199                             if not num_re.match(entry):
200                                 try:
201                                     entry = self.db.classes[link].lookup(entry)
202                                 except:
203                                     raise ValueError, \
204                                         'property "%s": %s not a %s'%(key,
205                                         entry, link)
206                             l.append(entry)
207                         l.sort()
208                         value = l
209                     # if changed, set it
210                     if value != cl.get(self.nodeid, key):
211                         changed.append(key)
212                         props[key] = value
213                 cl.set(self.nodeid, **props)
215                 # if this item has messages, generate an edit message
216                 # TODO: don't send the edit message to the person who
217                 # performed the edit
218                 if (cl.getprops().has_key('messages') and
219                         cl.getprops()['messages'].isMultilinkType and
220                         cl.getprops()['messages'].classname == 'msg'):
221                     nid = self.nodeid
222                     m = []
223                     for name, prop in cl.getprops().items():
224                         value = cl.get(nid, name)
225                         if prop.isLinkType:
226                             link = self.db.classes[prop.classname]
227                             key = link.getkey()
228                             if value is not None and key:
229                                 value = link.get(value, key)
230                             else:
231                                 value = '-'
232                         elif prop.isMultilinkType:
233                             l = []
234                             link = self.db.classes[prop.classname]
235                             for entry in value:
236                                 key = link.getkey()
237                                 if key:
238                                     l.append(link.get(entry, link.getkey()))
239                                 else:
240                                     l.append(entry)
241                             value = ', '.join(l)
242                         if name in changed:
243                             chg = '*'
244                         else:
245                             chg = ' '
246                         m.append('%s %s: %s'%(chg, name, value))
248                     # handle the note
249                     if self.form.has_key('__note'):
250                         note = self.form['__note'].value
251                         if '\n' in note:
252                             summary = re.split(r'\n\r?', note)[0]
253                         else:
254                             summary = note
255                         m.append('\n%s\n'%note)
256                     else:
257                         if len(changed) > 1:
258                             plural = 's were'
259                         else:
260                             plural = ' was'
261                         summary = 'This %s has been edited through the web '\
262                             'and the %s value%s changed.'%(cn,
263                             ', '.join(changed), plural)
264                         m.append('\n%s\n'%summary)
266                     # now create the message
267                     content = '\n'.join(m)
268                     message_id = self.db.msg.create(author=1, recipients=[],
269                         date=date.Date('.'), summary=summary, content=content)
270                     messages = cl.get(nid, 'messages')
271                     messages.append(message_id)
272                     props = {'messages': messages}
273                     cl.set(nid, **props)
275                 # and some nice feedback for the user
276                 message = '%s edited ok'%', '.join(changed)
277             except:
278                 s = StringIO.StringIO()
279                 traceback.print_exc(None, s)
280                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
282         # now the display
283         id = self.nodeid
284         if cl.getkey():
285             id = cl.get(id, cl.getkey())
286         self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
288         nodeid = self.nodeid
290         # use the template to display the item
291         template.item(self, self.db, self.classname, nodeid)
292         self.pagefoot()
293     showissue = showitem
294     showmsg = showitem
296     def newissue(self, message=None):
297         ''' add an issue
298         '''
299         cn = self.classname
300         cl = self.db.classes[cn]
302         # possibly perform a create
303         keys = self.form.keys()
304         num_re = re.compile('^\d+$')
305         if keys:
306             props = {}
307             try:
308                 keys = self.form.keys()
309                 for key in keys:
310                     if not cl.properties.has_key(key):
311                         continue
312                     proptype = cl.properties[key]
313                     if proptype.isStringType:
314                         value = str(self.form[key].value).strip()
315                     elif proptype.isDateType:
316                         value = date.Date(str(self.form[key].value))
317                     elif proptype.isIntervalType:
318                         value = date.Interval(str(self.form[key].value))
319                     elif proptype.isLinkType:
320                         value = str(self.form[key].value).strip()
321                         # handle key values
322                         link = cl.properties[key].classname
323                         if not num_re.match(value):
324                             try:
325                                 value = self.db.classes[link].lookup(value)
326                             except:
327                                 raise ValueError, 'property "%s": %s not a %s'%(
328                                     key, value, link)
329                     elif proptype.isMultilinkType:
330                         value = self.form[key]
331                         if type(value) != type([]):
332                             value = [i.strip() for i in str(value.value).split(',')]
333                         else:
334                             value = [str(i.value).strip() for i in value]
335                         link = cl.properties[key].classname
336                         l = []
337                         for entry in map(str, value):
338                             if not num_re.match(entry):
339                                 try:
340                                     entry = self.db.classes[link].lookup(entry)
341                                 except:
342                                     raise ValueError, \
343                                         'property "%s": %s not a %s'%(key,
344                                         entry, link)
345                             l.append(entry)
346                         l.sort()
347                         value = l
348                     props[key] = value
349                 nid = cl.create(**props)
351                 # if this item has messages, 
352                 if (cl.getprops().has_key('messages') and
353                         cl.getprops()['messages'].isMultilinkType and
354                         cl.getprops()['messages'].classname == 'msg'):
355                     # generate an edit message - nosyreactor will send it
356                     m = []
357                     for name, prop in cl.getprops().items():
358                         value = cl.get(nid, name)
359                         if prop.isLinkType:
360                             link = self.db.classes[prop.classname]
361                             key = link.getkey()
362                             if value is not None and key:
363                                 value = link.get(value, key)
364                             else:
365                                 value = '-'
366                         elif prop.isMultilinkType:
367                             l = []
368                             link = self.db.classes[prop.classname]
369                             for entry in value:
370                                 key = link.getkey()
371                                 if key:
372                                     l.append(link.get(entry, link.getkey()))
373                                 else:
374                                     l.append(entry)
375                             value = ', '.join(l)
376                         m.append('%s: %s'%(name, value))
378                     # handle the note
379                     if self.form.has_key('__note'):
380                         note = self.form['__note'].value
381                         if '\n' in note:
382                             summary = re.split(r'\n\r?', note)[0]
383                         else:
384                             summary = note
385                         m.append('\n%s\n'%note)
386                     else:
387                         if len(changed) > 1:
388                             plural = 's were'
389                         else:
390                             plural = ' was'
391                         summary = 'This %s has been created through the web.'%cn
392                         m.append('\n%s\n'%summary)
394                     # now create the message
395                     content = '\n'.join(m)
396                     message_id = self.db.msg.create(author=1, recipients=[],
397                         date=date.Date('.'), summary=summary, content=content)
398                     messages = cl.get(nid, 'messages')
399                     messages.append(message_id)
400                     props = {'messages': messages}
401                     cl.set(nid, **props)
403                 # and some nice feedback for the user
404                 message = '%s created ok'%cn
405             except:
406                 s = StringIO.StringIO()
407                 traceback.print_exc(None, s)
408                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
409         self.pagehead('New %s'%self.classname.capitalize(), message)
410         template.newitem(self, self.db, self.classname, self.form)
411         self.pagefoot()
413     def showuser(self, message=None):
414         ''' display an item
415         '''
416         if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
417             self.showitem(message)
418         else:
419             raise Unauthorised
421     def showfile(self):
422         ''' display a file
423         '''
424         nodeid = self.nodeid
425         cl = self.db.file
426         type = cl.get(nodeid, 'type')
427         if type == 'message/rfc822':
428             type = 'text/plain'
429         self.header(headers={'Content-Type': type})
430         self.write(cl.get(nodeid, 'content'))
432     def classes(self, message=None):
433         ''' display a list of all the classes in the database
434         '''
435         if self.user == 'admin':
436             self.pagehead('Table of classes', message)
437             classnames = self.db.classes.keys()
438             classnames.sort()
439             self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
440             for cn in classnames:
441                 cl = self.db.getclass(cn)
442                 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
443                 for key, value in cl.properties.items():
444                     if value is None: value = ''
445                     else: value = str(value)
446                     self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
447                         key, cgi.escape(value)))
448             self.write('</table>')
449             self.pagefoot()
450         else:
451             raise Unauthorised
453     def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
454             nre=re.compile(r'new(\w+)')):
455         path = self.split_path
456         if not path or path[0] in ('', 'index'):
457             self.index()
458         elif len(path) == 1:
459             if path[0] == 'list_classes':
460                 self.classes()
461                 return
462             m = dre.match(path[0])
463             if m:
464                 self.classname = m.group(1)
465                 self.nodeid = m.group(2)
466                 getattr(self, 'show%s'%self.classname)()
467                 return
468             m = nre.match(path[0])
469             if m:
470                 self.classname = m.group(1)
471                 getattr(self, 'new%s'%self.classname)()
472                 return
473             self.classname = path[0]
474             self.list()
475         else:
476             raise 'ValueError', 'Path not understood'
478     def __del__(self):
479         self.db.close()