Code

New issue form handles lack of note better now.
[roundup.git] / roundup / cgi_client.py
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()
492 # $Log: not supported by cvs2svn $
493 # Revision 1.4  2001/07/28 00:34:34  richard
494 # Fixed some non-string node ids.
496 # Revision 1.3  2001/07/23 03:56:30  richard
497 # oops, missed a config removal
499 # Revision 1.2  2001/07/22 12:09:32  richard
500 # Final commit of Grande Splite
502 # Revision 1.1  2001/07/22 11:58:35  richard
503 # More Grande Splite