Code

e46098ba0aa2273593bbb56c400825db403dddb4
[roundup.git] / roundup / cgi_client.py
1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17
18 # $Id: cgi_client.py,v 1.36 2001-10-21 04:44:50 richard Exp $
20 import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
21 import base64, Cookie, time
23 import roundupdb, htmltemplate, date, hyperdb, password
25 class Unauthorised(ValueError):
26     pass
28 class NotFound(ValueError):
29     pass
31 class Client:
32     '''
33     A note about login
34     ------------------
36     If the user has no login cookie, then they are anonymous. There
37     are two levels of anonymous use. If there is no 'anonymous' user, there
38     is no login at all and the database is opened in read-only mode. If the
39     'anonymous' user exists, the user is logged in using that user (though
40     there is no cookie). This allows them to modify the database, and all
41     modifications are attributed to the 'anonymous' user.
42     '''
44     def __init__(self, instance, out, env):
45         self.instance = instance
46         self.out = out
47         self.env = env
48         self.path = env['PATH_INFO']
49         self.split_path = self.path.split('/')
51         self.headers_done = 0
52         self.form = cgi.FieldStorage(environ=env)
53         self.headers_done = 0
54         self.debug = 0
56     def getuid(self):
57         return self.db.user.lookup(self.user)
59     def header(self, headers={'Content-Type':'text/html'}):
60         if not headers.has_key('Content-Type'):
61             headers['Content-Type'] = 'text/html'
62         for entry in headers.items():
63             self.out.write('%s: %s\n'%entry)
64         self.out.write('\n')
65         self.headers_done = 1
67     def pagehead(self, title, message=None):
68         url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
69         machine = self.env['SERVER_NAME']
70         port = self.env['SERVER_PORT']
71         if port != '80': machine = machine + ':' + port
72         base = urlparse.urlunparse(('http', machine, url, None, None, None))
73         if message is not None:
74             message = '<div class="system-msg">%s</div>'%message
75         else:
76             message = ''
77         style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
78         if self.user is not None:
79             userid = self.db.user.lookup(self.user)
80             user_info = '(login: <a href="user%s">%s</a>)'%(userid, self.user)
81         else:
82             user_info = ''
83         self.write('''<html><head>
84 <title>%s</title>
85 <style type="text/css">%s</style>
86 </head>
87 <body bgcolor=#ffffff>
88 %s
89 <table width=100%% border=0 cellspacing=0 cellpadding=2>
90 <tr class="location-bar"><td><big><strong>%s</strong></big> %s</td></tr>
91 </table>
92 '''%(title, style, message, title, user_info))
94     def pagefoot(self):
95         if self.debug:
96             self.write('<hr><small><dl>')
97             self.write('<dt><b>Path</b></dt>')
98             self.write('<dd>%s</dd>'%(', '.join(map(repr, self.split_path))))
99             keys = self.form.keys()
100             keys.sort()
101             if keys:
102                 self.write('<dt><b>Form entries</b></dt>')
103                 for k in self.form.keys():
104                     v = str(self.form[k].value)
105                     self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
106             keys = self.env.keys()
107             keys.sort()
108             self.write('<dt><b>CGI environment</b></dt>')
109             for k in keys:
110                 v = self.env[k]
111                 self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
112             self.write('</dl></small>')
113         self.write('</body></html>')
115     def write(self, content):
116         if not self.headers_done:
117             self.header()
118         self.out.write(content)
120     def index_arg(self, arg):
121         ''' handle the args to index - they might be a list from the form
122             (ie. submitted from a form) or they might be a command-separated
123             single string (ie. manually constructed GET args)
124         '''
125         if self.form.has_key(arg):
126             arg =  self.form[arg]
127             if type(arg) == type([]):
128                 return [arg.value for arg in arg]
129             return arg.value.split(',')
130         return []
132     def index_filterspec(self, filter):
133         ''' pull the index filter spec from the form
135         Links and multilinks want to be lists - the rest are straight
136         strings.
137         '''
138         props = self.db.classes[self.classname].getprops()
139         # all the form args not starting with ':' are filters
140         filterspec = {}
141         for key in self.form.keys():
142             if key[0] == ':': continue
143             if not props.has_key(key): continue
144             if key not in filter: continue
145             prop = props[key]
146             value = self.form[key]
147             if (isinstance(prop, hyperdb.Link) or
148                     isinstance(prop, hyperdb.Multilink)):
149                 if type(value) == type([]):
150                     value = [arg.value for arg in value]
151                 else:
152                     value = value.value.split(',')
153                 l = filterspec.get(key, [])
154                 l = l + value
155                 filterspec[key] = l
156             else:
157                 filterspec[key] = value.value
158         return filterspec
160     def customization_widget(self):
161         ''' The customization widget is visible by default. The widget
162             visibility is remembered by show_customization.  Visibility
163             is not toggled if the action value is "Redisplay"
164         '''
165         if not self.form.has_key('show_customization'):
166             visible = 1
167         else:
168             visible = int(self.form['show_customization'].value)
169             if self.form.has_key('action'):
170                 if self.form['action'].value != 'Redisplay':
171                     visible = self.form['action'].value == '+'
172             
173         return visible
175     default_index_sort = ['-activity']
176     default_index_group = ['priority']
177     default_index_filter = ['status']
178     default_index_columns = ['id','activity','title','status','assignedto']
179     default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
180     def index(self):
181         ''' put up an index
182         '''
183         self.classname = 'issue'
184         # see if the web has supplied us with any customisation info
185         defaults = 1
186         for key in ':sort', ':group', ':filter', ':columns':
187             if self.form.has_key(key):
188                 defaults = 0
189                 break
190         if defaults:
191             # no info supplied - use the defaults
192             sort = self.default_index_sort
193             group = self.default_index_group
194             filter = self.default_index_filter
195             columns = self.default_index_columns
196             filterspec = self.default_index_filterspec
197         else:
198             sort = self.index_arg(':sort')
199             group = self.index_arg(':group')
200             filter = self.index_arg(':filter')
201             columns = self.index_arg(':columns')
202             filterspec = self.index_filterspec(filter)
203         return self.list(columns=columns, filter=filter, group=group,
204             sort=sort, filterspec=filterspec)
206     # XXX deviates from spec - loses the '+' (that's a reserved character
207     # in URLS
208     def list(self, sort=None, group=None, filter=None, columns=None,
209             filterspec=None, show_customization=None):
210         ''' call the template index with the args
212             :sort    - sort by prop name, optionally preceeded with '-'
213                      to give descending or nothing for ascending sorting.
214             :group   - group by prop name, optionally preceeded with '-' or
215                      to sort in descending or nothing for ascending order.
216             :filter  - selects which props should be displayed in the filter
217                      section. Default is all.
218             :columns - selects the columns that should be displayed.
219                      Default is all.
221         '''
222         cn = self.classname
223         self.pagehead('Index of %s'%cn)
224         if sort is None: sort = self.index_arg(':sort')
225         if group is None: group = self.index_arg(':group')
226         if filter is None: filter = self.index_arg(':filter')
227         if columns is None: columns = self.index_arg(':columns')
228         if filterspec is None: filterspec = self.index_filterspec(filter)
229         if show_customization is None:
230             show_customization = self.customization_widget()
232         htmltemplate.index(self, self.TEMPLATES, self.db, cn, filterspec,
233             filter, columns, sort, group,
234             show_customization=show_customization)
235         self.pagefoot()
237     def shownode(self, message=None):
238         ''' display an item
239         '''
240         cn = self.classname
241         cl = self.db.classes[cn]
243         # possibly perform an edit
244         keys = self.form.keys()
245         num_re = re.compile('^\d+$')
246         if keys:
247             try:
248                 props, changed = parsePropsFromForm(self.db, cl, self.form,
249                     self.nodeid)
250                 cl.set(self.nodeid, **props)
251                 self._post_editnode(self.nodeid, changed)
252                 # and some nice feedback for the user
253                 message = '%s edited ok'%', '.join(changed)
254             except:
255                 s = StringIO.StringIO()
256                 traceback.print_exc(None, s)
257                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
259         # now the display
260         id = self.nodeid
261         if cl.getkey():
262             id = cl.get(id, cl.getkey())
263         self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
265         nodeid = self.nodeid
267         # use the template to display the item
268         htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid)
269         self.pagefoot()
270     showissue = shownode
271     showmsg = shownode
273     def showuser(self, message=None):
274         ''' display an item
275         '''
276         if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
277             self.shownode(message)
278         else:
279             raise Unauthorised
281     def showfile(self):
282         ''' display a file
283         '''
284         nodeid = self.nodeid
285         cl = self.db.file
286         type = cl.get(nodeid, 'type')
287         if type == 'message/rfc822':
288             type = 'text/plain'
289         self.header(headers={'Content-Type': type})
290         self.write(cl.get(nodeid, 'content'))
292     def _createnode(self):
293         ''' create a node based on the contents of the form
294         '''
295         cl = self.db.classes[self.classname]
296         props, dummy = parsePropsFromForm(self.db, cl, self.form)
297         return cl.create(**props)
299     def _post_editnode(self, nid, changes=None):
300         ''' do the linking and message sending part of the node creation
301         '''
302         cn = self.classname
303         cl = self.db.classes[cn]
304         # link if necessary
305         keys = self.form.keys()
306         for key in keys:
307             if key == ':multilink':
308                 value = self.form[key].value
309                 if type(value) != type([]): value = [value]
310                 for value in value:
311                     designator, property = value.split(':')
312                     link, nodeid = roundupdb.splitDesignator(designator)
313                     link = self.db.classes[link]
314                     value = link.get(nodeid, property)
315                     value.append(nid)
316                     link.set(nodeid, **{property: value})
317             elif key == ':link':
318                 value = self.form[key].value
319                 if type(value) != type([]): value = [value]
320                 for value in value:
321                     designator, property = value.split(':')
322                     link, nodeid = roundupdb.splitDesignator(designator)
323                     link = self.db.classes[link]
324                     link.set(nodeid, **{property: nid})
326         # generate an edit message
327         # don't bother if there's no messages or nosy list 
328         props = cl.getprops()
329         note = None
330         if self.form.has_key('__note'):
331             note = self.form['__note']
332             note = note.value
333         send = len(cl.get(nid, 'nosy', [])) or note
334         if (send and props.has_key('messages') and
335                 isinstance(props['messages'], hyperdb.Multilink) and
336                 props['messages'].classname == 'msg'):
338             # handle the note
339             if note:
340                 if '\n' in note:
341                     summary = re.split(r'\n\r?', note)[0]
342                 else:
343                     summary = note
344                 m = ['%s\n'%note]
345             else:
346                 summary = 'This %s has been edited through the web.\n'%cn
347                 m = [summary]
349             first = 1
350             for name, prop in props.items():
351                 if changes is not None and name not in changes: continue
352                 if first:
353                     m.append('\n-------')
354                     first = 0
355                 value = cl.get(nid, name, None)
356                 if isinstance(prop, hyperdb.Link):
357                     link = self.db.classes[prop.classname]
358                     key = link.labelprop(default_to_id=1)
359                     if value is not None and key:
360                         value = link.get(value, key)
361                     else:
362                         value = '-'
363                 elif isinstance(prop, hyperdb.Multilink):
364                     if value is None: value = []
365                     l = []
366                     link = self.db.classes[prop.classname]
367                     key = link.labelprop(default_to_id=1)
368                     for entry in value:
369                         if key:
370                             l.append(link.get(entry, key))
371                         else:
372                             l.append(entry)
373                     value = ', '.join(l)
374                 m.append('%s: %s'%(name, value))
376             # now create the message
377             content = '\n'.join(m)
378             message_id = self.db.msg.create(author=self.getuid(),
379                 recipients=[], date=date.Date('.'), summary=summary,
380                 content=content)
381             messages = cl.get(nid, 'messages')
382             messages.append(message_id)
383             props = {'messages': messages}
384             cl.set(nid, **props)
386     def newnode(self, message=None):
387         ''' Add a new node to the database.
388         
389         The form works in two modes: blank form and submission (that is,
390         the submission goes to the same URL). **Eventually this means that
391         the form will have previously entered information in it if
392         submission fails.
394         The new node will be created with the properties specified in the
395         form submission. For multilinks, multiple form entries are handled,
396         as are prop=value,value,value. You can't mix them though.
398         If the new node is to be referenced from somewhere else immediately
399         (ie. the new node is a file that is to be attached to a support
400         issue) then supply one of these arguments in addition to the usual
401         form entries:
402             :link=designator:property
403             :multilink=designator:property
404         ... which means that once the new node is created, the "property"
405         on the node given by "designator" should now reference the new
406         node's id. The node id will be appended to the multilink.
407         '''
408         cn = self.classname
409         cl = self.db.classes[cn]
411         # possibly perform a create
412         keys = self.form.keys()
413         if [i for i in keys if i[0] != ':']:
414             props = {}
415             try:
416                 nid = self._createnode()
417                 self._post_editnode(nid)
418                 # and some nice feedback for the user
419                 message = '%s created ok'%cn
420             except:
421                 s = StringIO.StringIO()
422                 traceback.print_exc(None, s)
423                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
424         self.pagehead('New %s'%self.classname.capitalize(), message)
425         htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
426             self.form)
427         self.pagefoot()
428     newissue = newnode
429     newuser = newnode
431     def newfile(self, message=None):
432         ''' Add a new file to the database.
433         
434         This form works very much the same way as newnode - it just has a
435         file upload.
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             try:
444                 file = self.form['content']
445                 type = mimetypes.guess_type(file.filename)[0]
446                 if not type:
447                     type = "application/octet-stream"
448                 self._post_editnode(cl.create(content=file.file.read(),
449                     type=type, name=file.filename))
450                 # and some nice feedback for the user
451                 message = '%s created ok'%cn
452             except:
453                 s = StringIO.StringIO()
454                 traceback.print_exc(None, s)
455                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
457         self.pagehead('New %s'%self.classname.capitalize(), message)
458         htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
459             self.form)
460         self.pagefoot()
462     def classes(self, message=None):
463         ''' display a list of all the classes in the database
464         '''
465         if self.user == 'admin':
466             self.pagehead('Table of classes', message)
467             classnames = self.db.classes.keys()
468             classnames.sort()
469             self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
470             for cn in classnames:
471                 cl = self.db.getclass(cn)
472                 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
473                 for key, value in cl.properties.items():
474                     if value is None: value = ''
475                     else: value = str(value)
476                     self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
477                         key, cgi.escape(value)))
478             self.write('</table>')
479             self.pagefoot()
480         else:
481             raise Unauthorised
483     def login(self, message=None):
484         self.pagehead('Login to roundup', message)
485         self.write('''
486 <table>
487 <tr><td colspan=2 class="strong-header">Existing User Login</td></tr>
488 <form action="login_action" method=POST>
489 <tr><td align=right>Login name: </td>
490     <td><input name="__login_name"></td></tr>
491 <tr><td align=right>Password: </td>
492     <td><input type="password" name="__login_password"></td></tr>
493 <tr><td></td>
494     <td><input type="submit" value="Log In"></td></tr>
495 </form>
497 <p>
498 <tr><td colspan=2 class="strong-header">New User Registration</td></tr>
499 <tr><td colspan=2><em>marked items</em> are optional...</td></tr>
500 <form action="newuser_action" method=POST>
501 <tr><td align=right><em>Name: </em></td>
502     <td><input name="__newuser_realname"></td></tr>
503 <tr><td align=right><em>Organisation: </em></td>
504     <td><input name="__newuser_organisation"></td></tr>
505 <tr><td align=right>E-Mail Address: </td>
506     <td><input name="__newuser_address"></td></tr>
507 <tr><td align=right><em>Phone: </em></td>
508     <td><input name="__newuser_phone"></td></tr>
509 <tr><td align=right>Preferred Login name: </td>
510     <td><input name="__newuser_username"></td></tr>
511 <tr><td align=right>Password: </td>
512     <td><input type="password" name="__newuser_password"></td></tr>
513 <tr><td align=right>Password Again: </td>
514     <td><input type="password" name="__newuser_confirm"></td></tr>
515 <tr><td></td>
516     <td><input type="submit" value="Register"></td></tr>
517 </form>
518 </table>
519 ''')
521     def login_action(self, message=None):
522         if not self.form.has_key('__login_name'):
523             return self.login(message='Username required')
524         self.user = self.form['__login_name'].value
525         if self.form.has_key('__login_password'):
526             password = self.form['__login_password'].value
527         else:
528             password = ''
529         # make sure the user exists
530         try:
531             uid = self.db.user.lookup(self.user)
532         except KeyError:
533             name = self.user
534             self.make_user_anonymous()
535             return self.login(message='No such user "%s"'%name)
537         # and that the password is correct
538         pw = self.db.user.get(uid, 'password')
539         if password != self.db.user.get(uid, 'password'):
540             self.make_user_anonymous()
541             return self.login(message='Incorrect password')
543         # construct the cookie
544         uid = self.db.user.lookup(self.user)
545         user = base64.encodestring('%s:%s'%(self.user, password))[:-1]
546         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
547             ''))
548         self.header({'Set-Cookie': 'roundup_user=%s; Path=%s;'%(user, path)})
549         return self.index()
551     def make_user_anonymous(self):
552         # make us anonymous if we can
553         try:
554             self.db.user.lookup('anonymous')
555             self.user = 'anonymous'
556         except KeyError:
557             self.user = None
559     def logout(self, message=None):
560         self.make_user_anonymous()
561         # construct the logout cookie
562         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
563             ''))
564         now = Cookie._getdate()
565         self.header({'Set-Cookie':
566             'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
567         return self.index()
569     def newuser_action(self, message=None):
570         ''' create a new user based on the contents of the form and then
571         set the cookie
572         '''
573         # TODO: pre-check the required fields and username key property
574         cl = self.db.classes['user']
575         props, dummy = parsePropsFromForm(self.db, cl, self.form)
576         uid = cl.create(**props)
577         self.user = self.db.user.get(uid, 'username')
578         password = self.db.user.get(uid, 'password')
579         # construct the cookie
580         uid = self.db.user.lookup(self.user)
581         user = base64.encodestring('%s:%s'%(self.user, password))[:-1]
582         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
583             ''))
584         self.header({'Set-Cookie': 'roundup_user=%s; Path=%s;'%(user, path)})
585         return self.index()
587     def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
588             nre=re.compile(r'new(\w+)')):
590         # determine the uid to use
591         self.db = self.instance.open('admin')
592         cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
593         user = 'anonymous'
594         if (cookie.has_key('roundup_user') and
595                 cookie['roundup_user'].value != 'deleted'):
596             cookie = cookie['roundup_user'].value
597             user, password = base64.decodestring(cookie).split(':')
598             # make sure the user exists
599             try:
600                 uid = self.db.user.lookup(user)
601                 # now validate the password
602                 if password != self.db.user.get(uid, 'password'):
603                     user = 'anonymous'
604             except KeyError:
605                 user = 'anonymous'
607         # make sure the anonymous user is valid if we're using it
608         if user == 'anonymous':
609             self.make_user_anonymous()
610         else:
611             self.user = user
612         self.db.close()
614         # re-open the database for real, using the user
615         self.db = self.instance.open(self.user)
617         # now figure which function to call
618         path = self.split_path
619         if not path or path[0] in ('', 'index'):
620             self.index()
621         elif len(path) == 1:
622             if path[0] == 'list_classes':
623                 self.classes()
624                 return
625             if path[0] == 'login':
626                 self.login()
627                 return
628             if path[0] == 'login_action':
629                 self.login_action()
630                 return
631             if path[0] == 'newuser_action':
632                 self.newuser_action()
633                 return
634             if path[0] == 'logout':
635                 self.logout()
636                 return
637             m = dre.match(path[0])
638             if m:
639                 self.classname = m.group(1)
640                 self.nodeid = m.group(2)
641                 try:
642                     cl = self.db.classes[self.classname]
643                 except KeyError:
644                     raise NotFound
645                 try:
646                     cl.get(self.nodeid, 'id')
647                 except IndexError:
648                     raise NotFound
649                 try:
650                     func = getattr(self, 'show%s'%self.classname)
651                 except AttributeError:
652                     raise NotFound
653                 func()
654                 return
655             m = nre.match(path[0])
656             if m:
657                 self.classname = m.group(1)
658                 try:
659                     func = getattr(self, 'new%s'%self.classname)
660                 except AttributeError:
661                     raise NotFound
662                 func()
663                 return
664             self.classname = path[0]
665             try:
666                 self.db.getclass(self.classname)
667             except KeyError:
668                 raise NotFound
669             self.list()
670         else:
671             raise 'ValueError', 'Path not understood'
673     def __del__(self):
674         self.db.close()
677 class ExtendedClient(Client): 
678     '''Includes pages and page heading information that relate to the
679        extended schema.
680     ''' 
681     showsupport = Client.shownode
682     showtimelog = Client.shownode
683     newsupport = Client.newnode
684     newtimelog = Client.newnode
686     default_index_sort = ['-activity']
687     default_index_group = ['priority']
688     default_index_filter = ['status']
689     default_index_columns = ['activity','status','title','assignedto']
690     default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
692     def pagehead(self, title, message=None):
693         url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
694         machine = self.env['SERVER_NAME']
695         port = self.env['SERVER_PORT']
696         if port != '80': machine = machine + ':' + port
697         base = urlparse.urlunparse(('http', machine, url, None, None, None))
698         if message is not None:
699             message = '<div class="system-msg">%s</div>'%message
700         else:
701             message = ''
702         style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
703         user_name = self.user or ''
704         if self.user == 'admin':
705             admin_links = ' | <a href="list_classes">Class List</a>'
706         else:
707             admin_links = ''
708         if self.user not in (None, 'anonymous'):
709             userid = self.db.user.lookup(self.user)
710             user_info = '''
711 <a href="issue?assignedto=%s&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:filter=status,assignedto&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">My Issues</a> |
712 <a href="support?assignedto=%s&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:filter=status,assignedto&:sort=activity&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">My Support</a> |
713 <a href="user%s">My Details</a> | <a href="logout">Logout</a>
714 '''%(userid, userid, userid)
715         else:
716             user_info = '<a href="login">Login</a>'
717         if self.user is not None:
718             add_links = '''
719 | Add
720 <a href="newissue">Issue</a>,
721 <a href="newsupport">Support</a>,
722 <a href="newuser">User</a>
723 '''
724         else:
725             add_links = ''
726         self.write('''<html><head>
727 <title>%s</title>
728 <style type="text/css">%s</style>
729 </head>
730 <body bgcolor=#ffffff>
731 %s
732 <table width=100%% border=0 cellspacing=0 cellpadding=2>
733 <tr class="location-bar"><td><big><strong>%s</strong></big></td>
734 <td align=right valign=bottom>%s</td></tr>
735 <tr class="location-bar">
736 <td align=left>All
737 <a href="issue?status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>,
738 <a href="support?status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">Support</a>
739 | Unassigned
740 <a href="issue?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>,
741 <a href="support?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">Support</a>
742 %s
743 %s</td>
744 <td align=right>%s</td>
745 </table>
746 '''%(title, style, message, title, user_name, add_links, admin_links,
747     user_info))
749 def parsePropsFromForm(db, cl, form, nodeid=0):
750     '''Pull properties for the given class out of the form.
751     '''
752     props = {}
753     changed = []
754     keys = form.keys()
755     num_re = re.compile('^\d+$')
756     for key in keys:
757         if not cl.properties.has_key(key):
758             continue
759         proptype = cl.properties[key]
760         if isinstance(proptype, hyperdb.String):
761             value = form[key].value.strip()
762         elif isinstance(proptype, hyperdb.Password):
763             value = password.Password(form[key].value.strip())
764         elif isinstance(proptype, hyperdb.Date):
765             value = date.Date(form[key].value.strip())
766         elif isinstance(proptype, hyperdb.Interval):
767             value = date.Interval(form[key].value.strip())
768         elif isinstance(proptype, hyperdb.Link):
769             value = form[key].value.strip()
770             # see if it's the "no selection" choice
771             if value == '-1':
772                 # don't set this property
773                 continue
774             else:
775                 # handle key values
776                 link = cl.properties[key].classname
777                 if not num_re.match(value):
778                     try:
779                         value = db.classes[link].lookup(value)
780                     except KeyError:
781                         raise ValueError, 'property "%s": %s not a %s'%(
782                             key, value, link)
783         elif isinstance(proptype, hyperdb.Multilink):
784             value = form[key]
785             if type(value) != type([]):
786                 value = [i.strip() for i in value.value.split(',')]
787             else:
788                 value = [i.value.strip() for i in value]
789             link = cl.properties[key].classname
790             l = []
791             for entry in map(str, value):
792                 if not num_re.match(entry):
793                     try:
794                         entry = db.classes[link].lookup(entry)
795                     except KeyError:
796                         raise ValueError, \
797                             'property "%s": "%s" not an entry of %s'%(key,
798                             entry, link.capitalize())
799                 l.append(entry)
800             l.sort()
801             value = l
802         props[key] = value
803         # if changed, set it
804         if nodeid and value != cl.get(nodeid, key):
805             changed.append(key)
806             props[key] = value
807     return props, changed
810 # $Log: not supported by cvs2svn $
811 # Revision 1.35  2001/10/21 00:17:54  richard
812 # CGI interface view customisation section may now be hidden (patch from
813 #  Roch'e Compaan.)
815 # Revision 1.34  2001/10/20 11:58:48  richard
816 # Catch errors in login - no username or password supplied.
817 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
819 # Revision 1.33  2001/10/17 00:18:41  richard
820 # Manually constructing cookie headers now.
822 # Revision 1.32  2001/10/16 03:36:21  richard
823 # CGI interface wasn't handling checkboxes at all.
825 # Revision 1.31  2001/10/14 10:55:00  richard
826 # Handle empty strings in HTML template Link function
828 # Revision 1.30  2001/10/09 07:38:58  richard
829 # Pushed the base code for the extended schema CGI interface back into the
830 # code cgi_client module so that future updates will be less painful.
831 # Also removed a debugging print statement from cgi_client.
833 # Revision 1.29  2001/10/09 07:25:59  richard
834 # Added the Password property type. See "pydoc roundup.password" for
835 # implementation details. Have updated some of the documentation too.
837 # Revision 1.28  2001/10/08 00:34:31  richard
838 # Change message was stuffing up for multilinks with no key property.
840 # Revision 1.27  2001/10/05 02:23:24  richard
841 #  . roundup-admin create now prompts for property info if none is supplied
842 #    on the command-line.
843 #  . hyperdb Class getprops() method may now return only the mutable
844 #    properties.
845 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
846 #    now support anonymous user access (read-only, unless there's an
847 #    "anonymous" user, in which case write access is permitted). Login
848 #    handling has been moved into cgi_client.Client.main()
849 #  . The "extended" schema is now the default in roundup init.
850 #  . The schemas have had their page headings modified to cope with the new
851 #    login handling. Existing installations should copy the interfaces.py
852 #    file from the roundup lib directory to their instance home.
853 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
854 #    Ping - has been removed.
855 #  . Fixed a whole bunch of places in the CGI interface where we should have
856 #    been returning Not Found instead of throwing an exception.
857 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
858 #    an item now throws an exception.
860 # Revision 1.26  2001/09/12 08:31:42  richard
861 # handle cases where mime type is not guessable
863 # Revision 1.25  2001/08/29 05:30:49  richard
864 # change messages weren't being saved when there was no-one on the nosy list.
866 # Revision 1.24  2001/08/29 04:49:39  richard
867 # didn't clean up fully after debugging :(
869 # Revision 1.23  2001/08/29 04:47:18  richard
870 # Fixed CGI client change messages so they actually include the properties
871 # changed (again).
873 # Revision 1.22  2001/08/17 00:08:10  richard
874 # reverted back to sending messages always regardless of who is doing the web
875 # edit. change notes weren't being saved. bleah. hackish.
877 # Revision 1.21  2001/08/15 23:43:18  richard
878 # Fixed some isFooTypes that I missed.
879 # Refactored some code in the CGI code.
881 # Revision 1.20  2001/08/12 06:32:36  richard
882 # using isinstance(blah, Foo) now instead of isFooType
884 # Revision 1.19  2001/08/07 00:24:42  richard
885 # stupid typo
887 # Revision 1.18  2001/08/07 00:15:51  richard
888 # Added the copyright/license notice to (nearly) all files at request of
889 # Bizar Software.
891 # Revision 1.17  2001/08/02 06:38:17  richard
892 # Roundupdb now appends "mailing list" information to its messages which
893 # include the e-mail address and web interface address. Templates may
894 # override this in their db classes to include specific information (support
895 # instructions, etc).
897 # Revision 1.16  2001/08/02 05:55:25  richard
898 # Web edit messages aren't sent to the person who did the edit any more. No
899 # message is generated if they are the only person on the nosy list.
901 # Revision 1.15  2001/08/02 00:34:10  richard
902 # bleah syntax error
904 # Revision 1.14  2001/08/02 00:26:16  richard
905 # Changed the order of the information in the message generated by web edits.
907 # Revision 1.13  2001/07/30 08:12:17  richard
908 # Added time logging and file uploading to the templates.
910 # Revision 1.12  2001/07/30 06:26:31  richard
911 # Added some documentation on how the newblah works.
913 # Revision 1.11  2001/07/30 06:17:45  richard
914 # Features:
915 #  . Added ability for cgi newblah forms to indicate that the new node
916 #    should be linked somewhere.
917 # Fixed:
918 #  . Fixed the agument handling for the roundup-admin find command.
919 #  . Fixed handling of summary when no note supplied for newblah. Again.
920 #  . Fixed detection of no form in htmltemplate Field display.
922 # Revision 1.10  2001/07/30 02:37:34  richard
923 # Temporary measure until we have decent schema migration...
925 # Revision 1.9  2001/07/30 01:25:07  richard
926 # Default implementation is now "classic" rather than "extended" as one would
927 # expect.
929 # Revision 1.8  2001/07/29 08:27:40  richard
930 # Fixed handling of passed-in values in form elements (ie. during a
931 # drill-down)
933 # Revision 1.7  2001/07/29 07:01:39  richard
934 # Added vim command to all source so that we don't get no steenkin' tabs :)
936 # Revision 1.6  2001/07/29 04:04:00  richard
937 # Moved some code around allowing for subclassing to change behaviour.
939 # Revision 1.5  2001/07/28 08:16:52  richard
940 # New issue form handles lack of note better now.
942 # Revision 1.4  2001/07/28 00:34:34  richard
943 # Fixed some non-string node ids.
945 # Revision 1.3  2001/07/23 03:56:30  richard
946 # oops, missed a config removal
948 # Revision 1.2  2001/07/22 12:09:32  richard
949 # Final commit of Grande Splite
951 # Revision 1.1  2001/07/22 11:58:35  richard
952 # More Grande Splite
955 # vim: set filetype=python ts=4 sw=4 et si