Code

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