Code

Fixed some non-string node ids.
[roundup.git] / roundup / cgi_client.py
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()
491 # $Log: not supported by cvs2svn $
492 # Revision 1.3  2001/07/23 03:56:30  richard
493 # oops, missed a config removal
495 # Revision 1.2  2001/07/22 12:09:32  richard
496 # Final commit of Grande Splite
498 # Revision 1.1  2001/07/22 11:58:35  richard
499 # More Grande Splite