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