Code

55ced042a158a93b77fd0e0016a7695d14c4afd7
[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.35 2001-10-21 00:17:54 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):
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             prop = props[key]
145             value = self.form[key]
146             if (isinstance(prop, hyperdb.Link) or
147                     isinstance(prop, hyperdb.Multilink)):
148                 if type(value) == type([]):
149                     value = [arg.value for arg in value]
150                 else:
151                     value = value.value.split(',')
152                 l = filterspec.get(key, [])
153                 l = l + value
154                 filterspec[key] = l
155             else:
156                 filterspec[key] = value.value
157         return filterspec
159     def customization_widget(self):
160         ''' The customization widget is visible by default. The widget
161             visibility is remembered by show_customization.  Visibility
162             is not toggled if the action value is "Redisplay"
163         '''
164         if not self.form.has_key('show_customization'):
165             visible = 1
166         else:
167             visible = int(self.form['show_customization'].value)
168             if self.form.has_key('action'):
169                 if self.form['action'].value != 'Redisplay':
170                     visible = self.form['action'].value == '+'
171             
172         return visible
174     default_index_sort = ['-activity']
175     default_index_group = ['priority']
176     default_index_filter = []
177     default_index_columns = ['id','activity','title','status','assignedto']
178     default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
179     def index(self):
180         ''' put up an index
181         '''
182         self.classname = 'issue'
183         if self.form.has_key(':sort'): sort = self.index_arg(':sort')
184         else: sort = self.default_index_sort
185         if self.form.has_key(':group'): group = self.index_arg(':group')
186         else: group = self.default_index_group
187         if self.form.has_key(':filter'): filter = self.index_arg(':filter')
188         else: filter = self.default_index_filter
189         if self.form.has_key(':columns'): columns = self.index_arg(':columns')
190         else: columns = self.default_index_columns
191         filterspec = self.index_filterspec()
192         if not filterspec:
193             filterspec = self.default_index_filterspec
194         return self.list(columns=columns, filter=filter, group=group,
195             sort=sort, filterspec=filterspec)
197     # XXX deviates from spec - loses the '+' (that's a reserved character
198     # in URLS
199     def list(self, sort=None, group=None, filter=None, columns=None,
200             filterspec=None, show_customization=None):
201         ''' call the template index with the args
203             :sort    - sort by prop name, optionally preceeded with '-'
204                      to give descending or nothing for ascending sorting.
205             :group   - group by prop name, optionally preceeded with '-' or
206                      to sort in descending or nothing for ascending order.
207             :filter  - selects which props should be displayed in the filter
208                      section. Default is all.
209             :columns - selects the columns that should be displayed.
210                      Default is all.
212         '''
213         cn = self.classname
214         self.pagehead('Index of %s'%cn)
215         if sort is None: sort = self.index_arg(':sort')
216         if group is None: group = self.index_arg(':group')
217         if filter is None: filter = self.index_arg(':filter')
218         if columns is None: columns = self.index_arg(':columns')
219         if filterspec is None: filterspec = self.index_filterspec()
220         if show_customization is None:
221             show_customization = self.customization_widget()
223         htmltemplate.index(self, self.TEMPLATES, self.db, cn, filterspec,
224             filter, columns, sort, group,
225             show_customization=show_customization)
226         self.pagefoot()
228     def shownode(self, message=None):
229         ''' display an item
230         '''
231         cn = self.classname
232         cl = self.db.classes[cn]
234         # possibly perform an edit
235         keys = self.form.keys()
236         num_re = re.compile('^\d+$')
237         if keys:
238             try:
239                 props, changed = parsePropsFromForm(self.db, cl, self.form,
240                     self.nodeid)
241                 cl.set(self.nodeid, **props)
242                 self._post_editnode(self.nodeid, changed)
243                 # and some nice feedback for the user
244                 message = '%s edited ok'%', '.join(changed)
245             except:
246                 s = StringIO.StringIO()
247                 traceback.print_exc(None, s)
248                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
250         # now the display
251         id = self.nodeid
252         if cl.getkey():
253             id = cl.get(id, cl.getkey())
254         self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
256         nodeid = self.nodeid
258         # use the template to display the item
259         htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid)
260         self.pagefoot()
261     showissue = shownode
262     showmsg = shownode
264     def showuser(self, message=None):
265         ''' display an item
266         '''
267         if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
268             self.shownode(message)
269         else:
270             raise Unauthorised
272     def showfile(self):
273         ''' display a file
274         '''
275         nodeid = self.nodeid
276         cl = self.db.file
277         type = cl.get(nodeid, 'type')
278         if type == 'message/rfc822':
279             type = 'text/plain'
280         self.header(headers={'Content-Type': type})
281         self.write(cl.get(nodeid, 'content'))
283     def _createnode(self):
284         ''' create a node based on the contents of the form
285         '''
286         cl = self.db.classes[self.classname]
287         props, dummy = parsePropsFromForm(self.db, cl, self.form)
288         return cl.create(**props)
290     def _post_editnode(self, nid, changes=None):
291         ''' do the linking and message sending part of the node creation
292         '''
293         cn = self.classname
294         cl = self.db.classes[cn]
295         # link if necessary
296         keys = self.form.keys()
297         for key in keys:
298             if key == ':multilink':
299                 value = self.form[key].value
300                 if type(value) != type([]): value = [value]
301                 for value in value:
302                     designator, property = value.split(':')
303                     link, nodeid = roundupdb.splitDesignator(designator)
304                     link = self.db.classes[link]
305                     value = link.get(nodeid, property)
306                     value.append(nid)
307                     link.set(nodeid, **{property: value})
308             elif key == ':link':
309                 value = self.form[key].value
310                 if type(value) != type([]): value = [value]
311                 for value in value:
312                     designator, property = value.split(':')
313                     link, nodeid = roundupdb.splitDesignator(designator)
314                     link = self.db.classes[link]
315                     link.set(nodeid, **{property: nid})
317         # generate an edit message
318         # don't bother if there's no messages or nosy list 
319         props = cl.getprops()
320         note = None
321         if self.form.has_key('__note'):
322             note = self.form['__note']
323             note = note.value
324         send = len(cl.get(nid, 'nosy', [])) or note
325         if (send and props.has_key('messages') and
326                 isinstance(props['messages'], hyperdb.Multilink) and
327                 props['messages'].classname == 'msg'):
329             # handle the note
330             if note:
331                 if '\n' in note:
332                     summary = re.split(r'\n\r?', note)[0]
333                 else:
334                     summary = note
335                 m = ['%s\n'%note]
336             else:
337                 summary = 'This %s has been edited through the web.\n'%cn
338                 m = [summary]
340             first = 1
341             for name, prop in props.items():
342                 if changes is not None and name not in changes: continue
343                 if first:
344                     m.append('\n-------')
345                     first = 0
346                 value = cl.get(nid, name, None)
347                 if isinstance(prop, hyperdb.Link):
348                     link = self.db.classes[prop.classname]
349                     key = link.labelprop(default_to_id=1)
350                     if value is not None and key:
351                         value = link.get(value, key)
352                     else:
353                         value = '-'
354                 elif isinstance(prop, hyperdb.Multilink):
355                     if value is None: value = []
356                     l = []
357                     link = self.db.classes[prop.classname]
358                     key = link.labelprop(default_to_id=1)
359                     for entry in value:
360                         if key:
361                             l.append(link.get(entry, key))
362                         else:
363                             l.append(entry)
364                     value = ', '.join(l)
365                 m.append('%s: %s'%(name, value))
367             # now create the message
368             content = '\n'.join(m)
369             message_id = self.db.msg.create(author=self.getuid(),
370                 recipients=[], date=date.Date('.'), summary=summary,
371                 content=content)
372             messages = cl.get(nid, 'messages')
373             messages.append(message_id)
374             props = {'messages': messages}
375             cl.set(nid, **props)
377     def newnode(self, message=None):
378         ''' Add a new node to the database.
379         
380         The form works in two modes: blank form and submission (that is,
381         the submission goes to the same URL). **Eventually this means that
382         the form will have previously entered information in it if
383         submission fails.
385         The new node will be created with the properties specified in the
386         form submission. For multilinks, multiple form entries are handled,
387         as are prop=value,value,value. You can't mix them though.
389         If the new node is to be referenced from somewhere else immediately
390         (ie. the new node is a file that is to be attached to a support
391         issue) then supply one of these arguments in addition to the usual
392         form entries:
393             :link=designator:property
394             :multilink=designator:property
395         ... which means that once the new node is created, the "property"
396         on the node given by "designator" should now reference the new
397         node's id. The node id will be appended to the multilink.
398         '''
399         cn = self.classname
400         cl = self.db.classes[cn]
402         # possibly perform a create
403         keys = self.form.keys()
404         if [i for i in keys if i[0] != ':']:
405             props = {}
406             try:
407                 nid = self._createnode()
408                 self._post_editnode(nid)
409                 # and some nice feedback for the user
410                 message = '%s created ok'%cn
411             except:
412                 s = StringIO.StringIO()
413                 traceback.print_exc(None, s)
414                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
415         self.pagehead('New %s'%self.classname.capitalize(), message)
416         htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
417             self.form)
418         self.pagefoot()
419     newissue = newnode
420     newuser = newnode
422     def newfile(self, message=None):
423         ''' Add a new file to the database.
424         
425         This form works very much the same way as newnode - it just has a
426         file upload.
427         '''
428         cn = self.classname
429         cl = self.db.classes[cn]
431         # possibly perform a create
432         keys = self.form.keys()
433         if [i for i in keys if i[0] != ':']:
434             try:
435                 file = self.form['content']
436                 type = mimetypes.guess_type(file.filename)[0]
437                 if not type:
438                     type = "application/octet-stream"
439                 self._post_editnode(cl.create(content=file.file.read(),
440                     type=type, name=file.filename))
441                 # and some nice feedback for the user
442                 message = '%s created ok'%cn
443             except:
444                 s = StringIO.StringIO()
445                 traceback.print_exc(None, s)
446                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
448         self.pagehead('New %s'%self.classname.capitalize(), message)
449         htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
450             self.form)
451         self.pagefoot()
453     def classes(self, message=None):
454         ''' display a list of all the classes in the database
455         '''
456         if self.user == 'admin':
457             self.pagehead('Table of classes', message)
458             classnames = self.db.classes.keys()
459             classnames.sort()
460             self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
461             for cn in classnames:
462                 cl = self.db.getclass(cn)
463                 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
464                 for key, value in cl.properties.items():
465                     if value is None: value = ''
466                     else: value = str(value)
467                     self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
468                         key, cgi.escape(value)))
469             self.write('</table>')
470             self.pagefoot()
471         else:
472             raise Unauthorised
474     def login(self, message=None):
475         self.pagehead('Login to roundup', message)
476         self.write('''
477 <table>
478 <tr><td colspan=2 class="strong-header">Existing User Login</td></tr>
479 <form action="login_action" method=POST>
480 <tr><td align=right>Login name: </td>
481     <td><input name="__login_name"></td></tr>
482 <tr><td align=right>Password: </td>
483     <td><input type="password" name="__login_password"></td></tr>
484 <tr><td></td>
485     <td><input type="submit" value="Log In"></td></tr>
486 </form>
488 <p>
489 <tr><td colspan=2 class="strong-header">New User Registration</td></tr>
490 <tr><td colspan=2><em>marked items</em> are optional...</td></tr>
491 <form action="newuser_action" method=POST>
492 <tr><td align=right><em>Name: </em></td>
493     <td><input name="__newuser_realname"></td></tr>
494 <tr><td align=right><em>Organisation: </em></td>
495     <td><input name="__newuser_organisation"></td></tr>
496 <tr><td align=right>E-Mail Address: </td>
497     <td><input name="__newuser_address"></td></tr>
498 <tr><td align=right><em>Phone: </em></td>
499     <td><input name="__newuser_phone"></td></tr>
500 <tr><td align=right>Preferred Login name: </td>
501     <td><input name="__newuser_username"></td></tr>
502 <tr><td align=right>Password: </td>
503     <td><input type="password" name="__newuser_password"></td></tr>
504 <tr><td align=right>Password Again: </td>
505     <td><input type="password" name="__newuser_confirm"></td></tr>
506 <tr><td></td>
507     <td><input type="submit" value="Register"></td></tr>
508 </form>
509 </table>
510 ''')
512     def login_action(self, message=None):
513         if not self.form.has_key('__login_name'):
514             return self.login(message='Username required')
515         self.user = self.form['__login_name'].value
516         if self.form.has_key('__login_password'):
517             password = self.form['__login_password'].value
518         else:
519             password = ''
520         # make sure the user exists
521         try:
522             uid = self.db.user.lookup(self.user)
523         except KeyError:
524             name = self.user
525             self.make_user_anonymous()
526             return self.login(message='No such user "%s"'%name)
528         # and that the password is correct
529         pw = self.db.user.get(uid, 'password')
530         if password != self.db.user.get(uid, 'password'):
531             self.make_user_anonymous()
532             return self.login(message='Incorrect password')
534         # construct the cookie
535         uid = self.db.user.lookup(self.user)
536         user = base64.encodestring('%s:%s'%(self.user, password))[:-1]
537         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
538             ''))
539         self.header({'Set-Cookie': 'roundup_user=%s; Path=%s;'%(user, path)})
540         return self.index()
542     def make_user_anonymous(self):
543         # make us anonymous if we can
544         try:
545             self.db.user.lookup('anonymous')
546             self.user = 'anonymous'
547         except KeyError:
548             self.user = None
550     def logout(self, message=None):
551         self.make_user_anonymous()
552         # construct the logout cookie
553         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
554             ''))
555         now = Cookie._getdate()
556         self.header({'Set-Cookie':
557             'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
558         return self.index()
560     def newuser_action(self, message=None):
561         ''' create a new user based on the contents of the form and then
562         set the cookie
563         '''
564         # TODO: pre-check the required fields and username key property
565         cl = self.db.classes['user']
566         props, dummy = parsePropsFromForm(self.db, cl, self.form)
567         uid = cl.create(**props)
568         self.user = self.db.user.get(uid, 'username')
569         password = self.db.user.get(uid, 'password')
570         # construct the cookie
571         uid = self.db.user.lookup(self.user)
572         user = base64.encodestring('%s:%s'%(self.user, password))[:-1]
573         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
574             ''))
575         self.header({'Set-Cookie': 'roundup_user=%s; Path=%s;'%(user, path)})
576         return self.index()
578     def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
579             nre=re.compile(r'new(\w+)')):
581         # determine the uid to use
582         self.db = self.instance.open('admin')
583         cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
584         user = 'anonymous'
585         if (cookie.has_key('roundup_user') and
586                 cookie['roundup_user'].value != 'deleted'):
587             cookie = cookie['roundup_user'].value
588             user, password = base64.decodestring(cookie).split(':')
589             # make sure the user exists
590             try:
591                 uid = self.db.user.lookup(user)
592                 # now validate the password
593                 if password != self.db.user.get(uid, 'password'):
594                     user = 'anonymous'
595             except KeyError:
596                 user = 'anonymous'
598         # make sure the anonymous user is valid if we're using it
599         if user == 'anonymous':
600             self.make_user_anonymous()
601         else:
602             self.user = user
603         self.db.close()
605         # re-open the database for real, using the user
606         self.db = self.instance.open(self.user)
608         # now figure which function to call
609         path = self.split_path
610         if not path or path[0] in ('', 'index'):
611             self.index()
612         elif len(path) == 1:
613             if path[0] == 'list_classes':
614                 self.classes()
615                 return
616             if path[0] == 'login':
617                 self.login()
618                 return
619             if path[0] == 'login_action':
620                 self.login_action()
621                 return
622             if path[0] == 'newuser_action':
623                 self.newuser_action()
624                 return
625             if path[0] == 'logout':
626                 self.logout()
627                 return
628             m = dre.match(path[0])
629             if m:
630                 self.classname = m.group(1)
631                 self.nodeid = m.group(2)
632                 try:
633                     cl = self.db.classes[self.classname]
634                 except KeyError:
635                     raise NotFound
636                 try:
637                     cl.get(self.nodeid, 'id')
638                 except IndexError:
639                     raise NotFound
640                 try:
641                     func = getattr(self, 'show%s'%self.classname)
642                 except AttributeError:
643                     raise NotFound
644                 func()
645                 return
646             m = nre.match(path[0])
647             if m:
648                 self.classname = m.group(1)
649                 try:
650                     func = getattr(self, 'new%s'%self.classname)
651                 except AttributeError:
652                     raise NotFound
653                 func()
654                 return
655             self.classname = path[0]
656             try:
657                 self.db.getclass(self.classname)
658             except KeyError:
659                 raise NotFound
660             self.list()
661         else:
662             raise 'ValueError', 'Path not understood'
664     def __del__(self):
665         self.db.close()
668 class ExtendedClient(Client): 
669     '''Includes pages and page heading information that relate to the
670        extended schema.
671     ''' 
672     showsupport = Client.shownode
673     showtimelog = Client.shownode
674     newsupport = Client.newnode
675     newtimelog = Client.newnode
677     default_index_sort = ['-activity']
678     default_index_group = ['priority']
679     default_index_filter = []
680     default_index_columns = ['activity','status','title','assignedto']
681     default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
683     def pagehead(self, title, message=None):
684         url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
685         machine = self.env['SERVER_NAME']
686         port = self.env['SERVER_PORT']
687         if port != '80': machine = machine + ':' + port
688         base = urlparse.urlunparse(('http', machine, url, None, None, None))
689         if message is not None:
690             message = '<div class="system-msg">%s</div>'%message
691         else:
692             message = ''
693         style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
694         user_name = self.user or ''
695         if self.user == 'admin':
696             admin_links = ' | <a href="list_classes">Class List</a>'
697         else:
698             admin_links = ''
699         if self.user not in (None, 'anonymous'):
700             userid = self.db.user.lookup(self.user)
701             user_info = '''
702 <a href="issue?assignedto=%s&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">My Issues</a> |
703 <a href="support?assignedto=%s&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">My Support</a> |
704 <a href="user%s">My Details</a> | <a href="logout">Logout</a>
705 '''%(userid, userid, userid)
706         else:
707             user_info = '<a href="login">Login</a>'
708         if self.user is not None:
709             add_links = '''
710 | Add
711 <a href="newissue">Issue</a>,
712 <a href="newsupport">Support</a>,
713 <a href="newuser">User</a>
714 '''
715         else:
716             add_links = ''
717         self.write('''<html><head>
718 <title>%s</title>
719 <style type="text/css">%s</style>
720 </head>
721 <body bgcolor=#ffffff>
722 %s
723 <table width=100%% border=0 cellspacing=0 cellpadding=2>
724 <tr class="location-bar"><td><big><strong>%s</strong></big></td>
725 <td align=right valign=bottom>%s</td></tr>
726 <tr class="location-bar">
727 <td align=left>All
728 <a href="issue?status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>,
729 <a href="support?status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">Support</a>
730 | Unassigned
731 <a href="issue?assignedto=admin&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>,
732 <a href="support?assignedto=admin&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">Support</a>
733 %s
734 %s</td>
735 <td align=right>%s</td>
736 </table>
737 '''%(title, style, message, title, user_name, add_links, admin_links,
738     user_info))
740 def parsePropsFromForm(db, cl, form, nodeid=0):
741     '''Pull properties for the given class out of the form.
742     '''
743     props = {}
744     changed = []
745     keys = form.keys()
746     num_re = re.compile('^\d+$')
747     for key in keys:
748         if not cl.properties.has_key(key):
749             continue
750         proptype = cl.properties[key]
751         if isinstance(proptype, hyperdb.String):
752             value = form[key].value.strip()
753         elif isinstance(proptype, hyperdb.Password):
754             value = password.Password(form[key].value.strip())
755         elif isinstance(proptype, hyperdb.Date):
756             value = date.Date(form[key].value.strip())
757         elif isinstance(proptype, hyperdb.Interval):
758             value = date.Interval(form[key].value.strip())
759         elif isinstance(proptype, hyperdb.Link):
760             value = form[key].value.strip()
761             # handle key values
762             link = cl.properties[key].classname
763             if not num_re.match(value):
764                 try:
765                     value = db.classes[link].lookup(value)
766                 except KeyError:
767                     raise ValueError, 'property "%s": %s not a %s'%(
768                         key, value, link)
769         elif isinstance(proptype, hyperdb.Multilink):
770             value = form[key]
771             if type(value) != type([]):
772                 value = [i.strip() for i in value.value.split(',')]
773             else:
774                 value = [i.value.strip() for i in value]
775             link = cl.properties[key].classname
776             l = []
777             for entry in map(str, value):
778                 if not num_re.match(entry):
779                     try:
780                         entry = db.classes[link].lookup(entry)
781                     except KeyError:
782                         raise ValueError, \
783                             'property "%s": "%s" not an entry of %s'%(key,
784                             entry, link.capitalize())
785                 l.append(entry)
786             l.sort()
787             value = l
788         props[key] = value
789         # if changed, set it
790         if nodeid and value != cl.get(nodeid, key):
791             changed.append(key)
792             props[key] = value
793     return props, changed
796 # $Log: not supported by cvs2svn $
797 # Revision 1.34  2001/10/20 11:58:48  richard
798 # Catch errors in login - no username or password supplied.
799 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
801 # Revision 1.33  2001/10/17 00:18:41  richard
802 # Manually constructing cookie headers now.
804 # Revision 1.32  2001/10/16 03:36:21  richard
805 # CGI interface wasn't handling checkboxes at all.
807 # Revision 1.31  2001/10/14 10:55:00  richard
808 # Handle empty strings in HTML template Link function
810 # Revision 1.30  2001/10/09 07:38:58  richard
811 # Pushed the base code for the extended schema CGI interface back into the
812 # code cgi_client module so that future updates will be less painful.
813 # Also removed a debugging print statement from cgi_client.
815 # Revision 1.29  2001/10/09 07:25:59  richard
816 # Added the Password property type. See "pydoc roundup.password" for
817 # implementation details. Have updated some of the documentation too.
819 # Revision 1.28  2001/10/08 00:34:31  richard
820 # Change message was stuffing up for multilinks with no key property.
822 # Revision 1.27  2001/10/05 02:23:24  richard
823 #  . roundup-admin create now prompts for property info if none is supplied
824 #    on the command-line.
825 #  . hyperdb Class getprops() method may now return only the mutable
826 #    properties.
827 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
828 #    now support anonymous user access (read-only, unless there's an
829 #    "anonymous" user, in which case write access is permitted). Login
830 #    handling has been moved into cgi_client.Client.main()
831 #  . The "extended" schema is now the default in roundup init.
832 #  . The schemas have had their page headings modified to cope with the new
833 #    login handling. Existing installations should copy the interfaces.py
834 #    file from the roundup lib directory to their instance home.
835 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
836 #    Ping - has been removed.
837 #  . Fixed a whole bunch of places in the CGI interface where we should have
838 #    been returning Not Found instead of throwing an exception.
839 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
840 #    an item now throws an exception.
842 # Revision 1.26  2001/09/12 08:31:42  richard
843 # handle cases where mime type is not guessable
845 # Revision 1.25  2001/08/29 05:30:49  richard
846 # change messages weren't being saved when there was no-one on the nosy list.
848 # Revision 1.24  2001/08/29 04:49:39  richard
849 # didn't clean up fully after debugging :(
851 # Revision 1.23  2001/08/29 04:47:18  richard
852 # Fixed CGI client change messages so they actually include the properties
853 # changed (again).
855 # Revision 1.22  2001/08/17 00:08:10  richard
856 # reverted back to sending messages always regardless of who is doing the web
857 # edit. change notes weren't being saved. bleah. hackish.
859 # Revision 1.21  2001/08/15 23:43:18  richard
860 # Fixed some isFooTypes that I missed.
861 # Refactored some code in the CGI code.
863 # Revision 1.20  2001/08/12 06:32:36  richard
864 # using isinstance(blah, Foo) now instead of isFooType
866 # Revision 1.19  2001/08/07 00:24:42  richard
867 # stupid typo
869 # Revision 1.18  2001/08/07 00:15:51  richard
870 # Added the copyright/license notice to (nearly) all files at request of
871 # Bizar Software.
873 # Revision 1.17  2001/08/02 06:38:17  richard
874 # Roundupdb now appends "mailing list" information to its messages which
875 # include the e-mail address and web interface address. Templates may
876 # override this in their db classes to include specific information (support
877 # instructions, etc).
879 # Revision 1.16  2001/08/02 05:55:25  richard
880 # Web edit messages aren't sent to the person who did the edit any more. No
881 # message is generated if they are the only person on the nosy list.
883 # Revision 1.15  2001/08/02 00:34:10  richard
884 # bleah syntax error
886 # Revision 1.14  2001/08/02 00:26:16  richard
887 # Changed the order of the information in the message generated by web edits.
889 # Revision 1.13  2001/07/30 08:12:17  richard
890 # Added time logging and file uploading to the templates.
892 # Revision 1.12  2001/07/30 06:26:31  richard
893 # Added some documentation on how the newblah works.
895 # Revision 1.11  2001/07/30 06:17:45  richard
896 # Features:
897 #  . Added ability for cgi newblah forms to indicate that the new node
898 #    should be linked somewhere.
899 # Fixed:
900 #  . Fixed the agument handling for the roundup-admin find command.
901 #  . Fixed handling of summary when no note supplied for newblah. Again.
902 #  . Fixed detection of no form in htmltemplate Field display.
904 # Revision 1.10  2001/07/30 02:37:34  richard
905 # Temporary measure until we have decent schema migration...
907 # Revision 1.9  2001/07/30 01:25:07  richard
908 # Default implementation is now "classic" rather than "extended" as one would
909 # expect.
911 # Revision 1.8  2001/07/29 08:27:40  richard
912 # Fixed handling of passed-in values in form elements (ie. during a
913 # drill-down)
915 # Revision 1.7  2001/07/29 07:01:39  richard
916 # Added vim command to all source so that we don't get no steenkin' tabs :)
918 # Revision 1.6  2001/07/29 04:04:00  richard
919 # Moved some code around allowing for subclassing to change behaviour.
921 # Revision 1.5  2001/07/28 08:16:52  richard
922 # New issue form handles lack of note better now.
924 # Revision 1.4  2001/07/28 00:34:34  richard
925 # Fixed some non-string node ids.
927 # Revision 1.3  2001/07/23 03:56:30  richard
928 # oops, missed a config removal
930 # Revision 1.2  2001/07/22 12:09:32  richard
931 # Final commit of Grande Splite
933 # Revision 1.1  2001/07/22 11:58:35  richard
934 # More Grande Splite
937 # vim: set filetype=python ts=4 sw=4 et si