Code

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