Code

3c497c068bb5ff3da8ab552cb4ac3abf673c172b
[roundup.git] / roundup / cgi_client.py
1 # $Id: cgi_client.py,v 1.16 2001-08-02 05:55:25 richard Exp $
3 import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
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 getuid(self):
25         return self.db.user.lookup(self.user)
27     def header(self, headers={'Content-Type':'text/html'}):
28         if not headers.has_key('Content-Type'):
29             headers['Content-Type'] = 'text/html'
30         for entry in headers.items():
31             self.out.write('%s: %s\n'%entry)
32         self.out.write('\n')
33         self.headers_done = 1
35     def pagehead(self, title, message=None):
36         url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
37         machine = self.env['SERVER_NAME']
38         port = self.env['SERVER_PORT']
39         if port != '80': machine = machine + ':' + port
40         base = urlparse.urlunparse(('http', machine, url, None, None, None))
41         if message is not None:
42             message = '<div class="system-msg">%s</div>'%message
43         else:
44             message = ''
45         style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
46         userid = self.db.user.lookup(self.user)
47         self.write('''<html><head>
48 <title>%s</title>
49 <style type="text/css">%s</style>
50 </head>
51 <body bgcolor=#ffffff>
52 %s
53 <table width=100%% border=0 cellspacing=0 cellpadding=2>
54 <tr class="location-bar"><td><big><strong>%s</strong></big>
55 (login: <a href="user%s">%s</a>)</td></tr>
56 </table>
57 '''%(title, style, message, title, userid, self.user))
59     def pagefoot(self):
60         if self.debug:
61             self.write('<hr><small><dl>')
62             self.write('<dt><b>Path</b></dt>')
63             self.write('<dd>%s</dd>'%(', '.join(map(repr, self.split_path))))
64             keys = self.form.keys()
65             keys.sort()
66             if keys:
67                 self.write('<dt><b>Form entries</b></dt>')
68                 for k in self.form.keys():
69                     v = str(self.form[k].value)
70                     self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
71             keys = self.env.keys()
72             keys.sort()
73             self.write('<dt><b>CGI environment</b></dt>')
74             for k in keys:
75                 v = self.env[k]
76                 self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
77             self.write('</dl></small>')
78         self.write('</body></html>')
80     def write(self, content):
81         if not self.headers_done:
82             self.header()
83         self.out.write(content)
85     def index_arg(self, arg):
86         ''' handle the args to index - they might be a list from the form
87             (ie. submitted from a form) or they might be a command-separated
88             single string (ie. manually constructed GET args)
89         '''
90         if self.form.has_key(arg):
91             arg =  self.form[arg]
92             if type(arg) == type([]):
93                 return [arg.value for arg in arg]
94             return arg.value.split(',')
95         return []
97     def index_filterspec(self):
98         ''' pull the index filter spec from the form
100         Links and multilinks want to be lists - the rest are straight
101         strings.
102         '''
103         props = self.db.classes[self.classname].getprops()
104         # all the form args not starting with ':' are filters
105         filterspec = {}
106         for key in self.form.keys():
107             if key[0] == ':': continue
108             prop = props[key]
109             value = self.form[key]
110             if prop.isLinkType or prop.isMultilinkType:
111                 if type(value) == type([]):
112                     value = [arg.value for arg in value]
113                 else:
114                     value = value.value.split(',')
115                 l = filterspec.get(key, [])
116                 l = l + value
117                 filterspec[key] = l
118             else:
119                 filterspec[key] = value.value
120         return filterspec
122     default_index_sort = ['-activity']
123     default_index_group = ['priority']
124     default_index_filter = []
125     default_index_columns = ['id','activity','title','status','assignedto']
126     default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
127     def index(self):
128         ''' put up an index
129         '''
130         self.classname = 'issue'
131         if self.form.has_key(':sort'): sort = self.index_arg(':sort')
132         else: sort = self.default_index_sort
133         if self.form.has_key(':group'): group = self.index_arg(':group')
134         else: group = self.default_index_group
135         if self.form.has_key(':filter'): filter = self.index_arg(':filter')
136         else: filter = self.default_index_filter
137         if self.form.has_key(':columns'): columns = self.index_arg(':columns')
138         else: columns = self.default_index_columns
139         filterspec = self.index_filterspec()
140         if not filterspec:
141             filterspec = self.default_index_filterspec
142         return self.list(columns=columns, filter=filter, group=group,
143             sort=sort, filterspec=filterspec)
145     # XXX deviates from spec - loses the '+' (that's a reserved character
146     # in URLS
147     def list(self, sort=None, group=None, filter=None, columns=None,
148             filterspec=None):
149         ''' call the template index with the args
151             :sort    - sort by prop name, optionally preceeded with '-'
152                      to give descending or nothing for ascending sorting.
153             :group   - group by prop name, optionally preceeded with '-' or
154                      to sort in descending or nothing for ascending order.
155             :filter  - selects which props should be displayed in the filter
156                      section. Default is all.
157             :columns - selects the columns that should be displayed.
158                      Default is all.
160         '''
161         cn = self.classname
162         self.pagehead('Index of %s'%cn)
163         if sort is None: sort = self.index_arg(':sort')
164         if group is None: group = self.index_arg(':group')
165         if filter is None: filter = self.index_arg(':filter')
166         if columns is None: columns = self.index_arg(':columns')
167         if filterspec is None: filterspec = self.index_filterspec()
169         htmltemplate.index(self, self.TEMPLATES, self.db, cn, filterspec,
170             filter, columns, sort, group)
171         self.pagefoot()
173     def shownode(self, message=None):
174         ''' display an item
175         '''
176         cn = self.classname
177         cl = self.db.classes[cn]
179         # possibly perform an edit
180         keys = self.form.keys()
181         num_re = re.compile('^\d+$')
182         if keys:
183             changed = []
184             props = {}
185             try:
186                 keys = self.form.keys()
187                 for key in keys:
188                     if not cl.properties.has_key(key):
189                         continue
190                     proptype = cl.properties[key]
191                     if proptype.isStringType:
192                         value = str(self.form[key].value).strip()
193                     elif proptype.isDateType:
194                         value = date.Date(str(self.form[key].value))
195                     elif proptype.isIntervalType:
196                         value = date.Interval(str(self.form[key].value))
197                     elif proptype.isLinkType:
198                         value = str(self.form[key].value).strip()
199                         # handle key values
200                         link = cl.properties[key].classname
201                         if not num_re.match(value):
202                             try:
203                                 value = self.db.classes[link].lookup(value)
204                             except:
205                                 raise ValueError, 'property "%s": %s not a %s'%(
206                                     key, value, link)
207                     elif proptype.isMultilinkType:
208                         value = self.form[key]
209                         if type(value) != type([]):
210                             value = [i.strip() for i in str(value.value).split(',')]
211                         else:
212                             value = [str(i.value).strip() for i in value]
213                         link = cl.properties[key].classname
214                         l = []
215                         for entry in map(str, value):
216                             if not num_re.match(entry):
217                                 try:
218                                     entry = self.db.classes[link].lookup(entry)
219                                 except:
220                                     raise ValueError, \
221                                         'property "%s": %s not a %s'%(key,
222                                         entry, link)
223                             l.append(entry)
224                         l.sort()
225                         value = l
226                     # if changed, set it
227                     if value != cl.get(self.nodeid, key):
228                         changed.append(key)
229                         props[key] = value
230                 cl.set(self.nodeid, **props)
232                 self._post_editnode(self.nodeid)
233                 # and some nice feedback for the user
234                 message = '%s edited ok'%', '.join(changed)
235             except:
236                 s = StringIO.StringIO()
237                 traceback.print_exc(None, s)
238                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
240         # now the display
241         id = self.nodeid
242         if cl.getkey():
243             id = cl.get(id, cl.getkey())
244         self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
246         nodeid = self.nodeid
248         # use the template to display the item
249         htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid)
250         self.pagefoot()
251     showissue = shownode
252     showmsg = shownode
254     def showuser(self, message=None):
255         ''' display an item
256         '''
257         if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
258             self.shownode(message)
259         else:
260             raise Unauthorised
262     def showfile(self):
263         ''' display a file
264         '''
265         nodeid = self.nodeid
266         cl = self.db.file
267         type = cl.get(nodeid, 'type')
268         if type == 'message/rfc822':
269             type = 'text/plain'
270         self.header(headers={'Content-Type': type})
271         self.write(cl.get(nodeid, 'content'))
273     def _createnode(self):
274         ''' create a node based on the contents of the form
275         '''
276         cn = self.classname
277         cl = self.db.classes[cn]
278         props = {}
279         keys = self.form.keys()
280         num_re = re.compile('^\d+$')
281         for key in keys:
282             if not cl.properties.has_key(key):
283                 continue
284             proptype = cl.properties[key]
285             if proptype.isStringType:
286                 value = self.form[key].value.strip()
287             elif proptype.isDateType:
288                 value = date.Date(self.form[key].value.strip())
289             elif proptype.isIntervalType:
290                 value = date.Interval(self.form[key].value.strip())
291             elif proptype.isLinkType:
292                 value = self.form[key].value.strip()
293                 # handle key values
294                 link = cl.properties[key].classname
295                 if not num_re.match(value):
296                     try:
297                         value = self.db.classes[link].lookup(value)
298                     except:
299                         raise ValueError, 'property "%s": %s not a %s'%(
300                             key, value, link)
301             elif proptype.isMultilinkType:
302                 value = self.form[key]
303                 if type(value) != type([]):
304                     value = [i.strip() for i in value.value.split(',')]
305                 else:
306                     value = [i.value.strip() for i in value]
307                 link = cl.properties[key].classname
308                 l = []
309                 for entry in map(str, value):
310                     if not num_re.match(entry):
311                         try:
312                             entry = self.db.classes[link].lookup(entry)
313                         except:
314                             raise ValueError, \
315                                 'property "%s": %s not a %s'%(key,
316                                 entry, link)
317                     l.append(entry)
318                 l.sort()
319                 value = l
320             props[key] = value
321         return cl.create(**props)
323     def _post_editnode(self, nid):
324         ''' do the linking and message sending part of the node creation
325         '''
326         cn = self.classname
327         cl = self.db.classes[cn]
328         # link if necessary
329         keys = self.form.keys()
330         for key in keys:
331             if key == ':multilink':
332                 value = self.form[key].value
333                 if type(value) != type([]): value = [value]
334                 for value in value:
335                     designator, property = value.split(':')
336                     link, nodeid = roundupdb.splitDesignator(designator)
337                     link = self.db.classes[link]
338                     value = link.get(nodeid, property)
339                     value.append(nid)
340                     link.set(nodeid, **{property: value})
341             elif key == ':link':
342                 value = self.form[key].value
343                 if type(value) != type([]): value = [value]
344                 for value in value:
345                     designator, property = value.split(':')
346                     link, nodeid = roundupdb.splitDesignator(designator)
347                     link = self.db.classes[link]
348                     link.set(nodeid, **{property: nid})
350         # see if we want to send a message to the nosy list...
351         props = cl.getprops()
352         # don't do the message thing if there's no nosy list, or the editor
353         # of the node is the only person on the nosy list - they're already
354         # aware of the change.
355         nosy = 0
356         if props.has_key('nosy'):
357             nosy = cl.get(nid, 'nosy')
358             uid = self.getuid()
359             if len(nosy) == 1 and uid in nosy:
360                 nosy = 0
361         if (nosy and props.has_key('messages') and
362                 props['messages'].isMultilinkType and
363                 props['messages'].classname == 'msg'):
365             # handle the note
366             note = None
367             if self.form.has_key('__note'):
368                 note = self.form['__note']
369             if note is not None and note.value:
370                 note = note.value
371                 if '\n' in note:
372                     summary = re.split(r'\n\r?', note)[0]
373                 else:
374                     summary = note
375                 m = ['%s\n'%note]
376             else:
377                 summary = 'This %s has been created through the web.\n'%cn
378                 m = [summary]
379             m.append('\n-------\n')
381             # generate an edit message - nosyreactor will send it
382             for name, prop in props.items():
383                 value = cl.get(nid, name, None)
384                 if prop.isLinkType:
385                     link = self.db.classes[prop.classname]
386                     key = link.getkey()
387                     if value is not None and key:
388                         value = link.get(value, key)
389                     else:
390                         value = '-'
391                 elif prop.isMultilinkType:
392                     if value is None: value = []
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             # now create the message
405             content = '\n'.join(m)
406             nosy.remove(self.getuid())
407             message_id = self.db.msg.create(author=self.getuid(),
408                 recipients=nosy, date=date.Date('.'), summary=summary,
409                 content=content)
410             messages = cl.get(nid, 'messages')
411             messages.append(message_id)
412             props = {'messages': messages}
413             cl.set(nid, **props)
415     def newnode(self, message=None):
416         ''' Add a new node to the database.
417         
418         The form works in two modes: blank form and submission (that is,
419         the submission goes to the same URL). **Eventually this means that
420         the form will have previously entered information in it if
421         submission fails.
423         The new node will be created with the properties specified in the
424         form submission. For multilinks, multiple form entries are handled,
425         as are prop=value,value,value. You can't mix them though.
427         If the new node is to be referenced from somewhere else immediately
428         (ie. the new node is a file that is to be attached to a support
429         issue) then supply one of these arguments in addition to the usual
430         form entries:
431             :link=designator:property
432             :multilink=designator:property
433         ... which means that once the new node is created, the "property"
434         on the node given by "designator" should now reference the new
435         node's id. The node id will be appended to the multilink.
436         '''
437         cn = self.classname
438         cl = self.db.classes[cn]
440         # possibly perform a create
441         keys = self.form.keys()
442         if [i for i in keys if i[0] != ':']:
443             props = {}
444             try:
445                 nid = self._createnode()
446                 self._post_editnode(nid)
447                 # and some nice feedback for the user
448                 message = '%s created ok'%cn
449             except:
450                 s = StringIO.StringIO()
451                 traceback.print_exc(None, s)
452                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
453         self.pagehead('New %s'%self.classname.capitalize(), message)
454         htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
455             self.form)
456         self.pagefoot()
457     newissue = newnode
458     newuser = newnode
460     def newfile(self, message=None):
461         ''' Add a new file to the database.
462         
463         This form works very much the same way as newnode - it just has a
464         file upload.
465         '''
466         cn = self.classname
467         cl = self.db.classes[cn]
469         # possibly perform a create
470         keys = self.form.keys()
471         if [i for i in keys if i[0] != ':']:
472             try:
473                 file = self.form['content']
474                 self._post_editnode(cl.create(content=file.file.read(),
475                     type=mimetypes.guess_type(file.filename)[0],
476                     name=file.filename))
477                 # and some nice feedback for the user
478                 message = '%s created ok'%cn
479             except:
480                 s = StringIO.StringIO()
481                 traceback.print_exc(None, s)
482                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
484         self.pagehead('New %s'%self.classname.capitalize(), message)
485         htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
486             self.form)
487         self.pagefoot()
489     def classes(self, message=None):
490         ''' display a list of all the classes in the database
491         '''
492         if self.user == 'admin':
493             self.pagehead('Table of classes', message)
494             classnames = self.db.classes.keys()
495             classnames.sort()
496             self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
497             for cn in classnames:
498                 cl = self.db.getclass(cn)
499                 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
500                 for key, value in cl.properties.items():
501                     if value is None: value = ''
502                     else: value = str(value)
503                     self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
504                         key, cgi.escape(value)))
505             self.write('</table>')
506             self.pagefoot()
507         else:
508             raise Unauthorised
510     def main(self, dre=re.compile(r'([^\d]+)(\d+)'), nre=re.compile(r'new(\w+)')):
511         path = self.split_path
512         if not path or path[0] in ('', 'index'):
513             self.index()
514         elif len(path) == 1:
515             if path[0] == 'list_classes':
516                 self.classes()
517                 return
518             m = dre.match(path[0])
519             if m:
520                 self.classname = m.group(1)
521                 self.nodeid = m.group(2)
522                 getattr(self, 'show%s'%self.classname)()
523                 return
524             m = nre.match(path[0])
525             if m:
526                 self.classname = m.group(1)
527                 getattr(self, 'new%s'%self.classname)()
528                 return
529             self.classname = path[0]
530             self.list()
531         else:
532             raise 'ValueError', 'Path not understood'
534     def __del__(self):
535         self.db.close()
538 # $Log: not supported by cvs2svn $
539 # Revision 1.15  2001/08/02 00:34:10  richard
540 # bleah syntax error
542 # Revision 1.14  2001/08/02 00:26:16  richard
543 # Changed the order of the information in the message generated by web edits.
545 # Revision 1.13  2001/07/30 08:12:17  richard
546 # Added time logging and file uploading to the templates.
548 # Revision 1.12  2001/07/30 06:26:31  richard
549 # Added some documentation on how the newblah works.
551 # Revision 1.11  2001/07/30 06:17:45  richard
552 # Features:
553 #  . Added ability for cgi newblah forms to indicate that the new node
554 #    should be linked somewhere.
555 # Fixed:
556 #  . Fixed the agument handling for the roundup-admin find command.
557 #  . Fixed handling of summary when no note supplied for newblah. Again.
558 #  . Fixed detection of no form in htmltemplate Field display.
560 # Revision 1.10  2001/07/30 02:37:34  richard
561 # Temporary measure until we have decent schema migration...
563 # Revision 1.9  2001/07/30 01:25:07  richard
564 # Default implementation is now "classic" rather than "extended" as one would
565 # expect.
567 # Revision 1.8  2001/07/29 08:27:40  richard
568 # Fixed handling of passed-in values in form elements (ie. during a
569 # drill-down)
571 # Revision 1.7  2001/07/29 07:01:39  richard
572 # Added vim command to all source so that we don't get no steenkin' tabs :)
574 # Revision 1.6  2001/07/29 04:04:00  richard
575 # Moved some code around allowing for subclassing to change behaviour.
577 # Revision 1.5  2001/07/28 08:16:52  richard
578 # New issue form handles lack of note better now.
580 # Revision 1.4  2001/07/28 00:34:34  richard
581 # Fixed some non-string node ids.
583 # Revision 1.3  2001/07/23 03:56:30  richard
584 # oops, missed a config removal
586 # Revision 1.2  2001/07/22 12:09:32  richard
587 # Final commit of Grande Splite
589 # Revision 1.1  2001/07/22 11:58:35  richard
590 # More Grande Splite
593 # vim: set filetype=python ts=4 sw=4 et si