Code

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