Code

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