c565c546d0823757bb23b5fe952cee1ad106c61b
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.49 2001-11-04 03:07:12 richard Exp $
20 import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
21 import binascii, 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 a user page for editing. Make sure the user is allowed
316 to edit this node, and also check for password changes.
317 '''
318 if self.user == 'anonymous':
319 raise Unauthorised
321 user = self.db.user
323 # get the username of the node being edited
324 node_user = user.get(self.nodeid, 'username')
326 if self.user not in ('admin', node_user):
327 raise Unauthorised
329 #
330 # perform any editing
331 #
332 keys = self.form.keys()
333 num_re = re.compile('^\d+$')
334 if keys:
335 try:
336 props, changed = parsePropsFromForm(self.db, user, self.form,
337 self.nodeid)
338 if self.nodeid == self.getuid() and 'password' in changed:
339 set_cookie = self.form['password'].value.strip()
340 else:
341 set_cookie = 0
342 user.set(self.nodeid, **props)
343 self._post_editnode(self.nodeid, changed)
344 # and some feedback for the user
345 message = '%s edited ok'%', '.join(changed)
346 except:
347 s = StringIO.StringIO()
348 traceback.print_exc(None, s)
349 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
350 else:
351 set_cookie = 0
353 # fix the cookie if the password has changed
354 if set_cookie:
355 self.set_cookie(self.user, set_cookie)
357 #
358 # now the display
359 #
360 self.pagehead('User: %s'%node_user, message)
362 # use the template to display the item
363 item = htmltemplate.ItemTemplate(self, self.TEMPLATES, 'user')
364 item.render(self.nodeid)
365 self.pagefoot()
367 def showfile(self):
368 ''' display a file
369 '''
370 nodeid = self.nodeid
371 cl = self.db.file
372 type = cl.get(nodeid, 'type')
373 if type == 'message/rfc822':
374 type = 'text/plain'
375 self.header(headers={'Content-Type': type})
376 self.write(cl.get(nodeid, 'content'))
378 def _createnode(self):
379 ''' create a node based on the contents of the form
380 '''
381 cl = self.db.classes[self.classname]
382 props, dummy = parsePropsFromForm(self.db, cl, self.form)
383 return cl.create(**props)
385 def _post_editnode(self, nid, changes=None):
386 ''' do the linking and message sending part of the node creation
387 '''
388 cn = self.classname
389 cl = self.db.classes[cn]
390 # link if necessary
391 keys = self.form.keys()
392 for key in keys:
393 if key == ':multilink':
394 value = self.form[key].value
395 if type(value) != type([]): value = [value]
396 for value in value:
397 designator, property = value.split(':')
398 link, nodeid = roundupdb.splitDesignator(designator)
399 link = self.db.classes[link]
400 value = link.get(nodeid, property)
401 value.append(nid)
402 link.set(nodeid, **{property: value})
403 elif key == ':link':
404 value = self.form[key].value
405 if type(value) != type([]): value = [value]
406 for value in value:
407 designator, property = value.split(':')
408 link, nodeid = roundupdb.splitDesignator(designator)
409 link = self.db.classes[link]
410 link.set(nodeid, **{property: nid})
412 # generate an edit message
413 # don't bother if there's no messages or nosy list
414 props = cl.getprops()
415 note = None
416 if self.form.has_key('__note'):
417 note = self.form['__note']
418 note = note.value
419 send = len(cl.get(nid, 'nosy', [])) or note
420 if (send and props.has_key('messages') and
421 isinstance(props['messages'], hyperdb.Multilink) and
422 props['messages'].classname == 'msg'):
424 # handle the note
425 if note:
426 if '\n' in note:
427 summary = re.split(r'\n\r?', note)[0]
428 else:
429 summary = note
430 m = ['%s\n'%note]
431 else:
432 summary = 'This %s has been edited through the web.\n'%cn
433 m = [summary]
435 first = 1
436 for name, prop in props.items():
437 if changes is not None and name not in changes: continue
438 if first:
439 m.append('\n-------')
440 first = 0
441 value = cl.get(nid, name, None)
442 if isinstance(prop, hyperdb.Link):
443 link = self.db.classes[prop.classname]
444 key = link.labelprop(default_to_id=1)
445 if value is not None and key:
446 value = link.get(value, key)
447 else:
448 value = '-'
449 elif isinstance(prop, hyperdb.Multilink):
450 if value is None: value = []
451 l = []
452 link = self.db.classes[prop.classname]
453 key = link.labelprop(default_to_id=1)
454 for entry in value:
455 if key:
456 l.append(link.get(entry, key))
457 else:
458 l.append(entry)
459 value = ', '.join(l)
460 m.append('%s: %s'%(name, value))
462 # now create the message
463 content = '\n'.join(m)
464 message_id = self.db.msg.create(author=self.getuid(),
465 recipients=[], date=date.Date('.'), summary=summary,
466 content=content)
467 messages = cl.get(nid, 'messages')
468 messages.append(message_id)
469 props = {'messages': messages}
470 cl.set(nid, **props)
472 def newnode(self, message=None):
473 ''' Add a new node to the database.
475 The form works in two modes: blank form and submission (that is,
476 the submission goes to the same URL). **Eventually this means that
477 the form will have previously entered information in it if
478 submission fails.
480 The new node will be created with the properties specified in the
481 form submission. For multilinks, multiple form entries are handled,
482 as are prop=value,value,value. You can't mix them though.
484 If the new node is to be referenced from somewhere else immediately
485 (ie. the new node is a file that is to be attached to a support
486 issue) then supply one of these arguments in addition to the usual
487 form entries:
488 :link=designator:property
489 :multilink=designator:property
490 ... which means that once the new node is created, the "property"
491 on the node given by "designator" should now reference the new
492 node's id. The node id will be appended to the multilink.
493 '''
494 cn = self.classname
495 cl = self.db.classes[cn]
497 # possibly perform a create
498 keys = self.form.keys()
499 if [i for i in keys if i[0] != ':']:
500 props = {}
501 try:
502 nid = self._createnode()
503 self._post_editnode(nid)
504 # and some nice feedback for the user
505 message = '%s created ok'%cn
506 except:
507 s = StringIO.StringIO()
508 traceback.print_exc(None, s)
509 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
510 self.pagehead('New %s'%self.classname.capitalize(), message)
512 # call the template
513 newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
514 self.classname)
515 newitem.render(self.form)
517 self.pagefoot()
518 newissue = newnode
519 newuser = newnode
521 def newfile(self, message=None):
522 ''' Add a new file to the database.
524 This form works very much the same way as newnode - it just has a
525 file upload.
526 '''
527 cn = self.classname
528 cl = self.db.classes[cn]
530 # possibly perform a create
531 keys = self.form.keys()
532 if [i for i in keys if i[0] != ':']:
533 try:
534 file = self.form['content']
535 type = mimetypes.guess_type(file.filename)[0]
536 if not type:
537 type = "application/octet-stream"
538 self._post_editnode(cl.create(content=file.file.read(),
539 type=type, name=file.filename))
540 # and some nice feedback for the user
541 message = '%s created ok'%cn
542 except:
543 s = StringIO.StringIO()
544 traceback.print_exc(None, s)
545 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
547 self.pagehead('New %s'%self.classname.capitalize(), message)
548 newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
549 self.classname)
550 newitem.render(self.form)
551 self.pagefoot()
553 def classes(self, message=None):
554 ''' display a list of all the classes in the database
555 '''
556 if self.user == 'admin':
557 self.pagehead('Table of classes', message)
558 classnames = self.db.classes.keys()
559 classnames.sort()
560 self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
561 for cn in classnames:
562 cl = self.db.getclass(cn)
563 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
564 for key, value in cl.properties.items():
565 if value is None: value = ''
566 else: value = str(value)
567 self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
568 key, cgi.escape(value)))
569 self.write('</table>')
570 self.pagefoot()
571 else:
572 raise Unauthorised
574 def login(self, message=None):
575 self.pagehead('Login to roundup', message)
576 self.write('''
577 <table>
578 <tr><td colspan=2 class="strong-header">Existing User Login</td></tr>
579 <form action="login_action" method=POST>
580 <tr><td align=right>Login name: </td>
581 <td><input name="__login_name"></td></tr>
582 <tr><td align=right>Password: </td>
583 <td><input type="password" name="__login_password"></td></tr>
584 <tr><td></td>
585 <td><input type="submit" value="Log In"></td></tr>
586 </form>
587 ''')
588 if self.user is None and self.ANONYMOUS_REGISTER == 'deny':
589 self.write('</table>')
590 self.pagefoot()
591 return
592 self.write('''
593 <p>
594 <tr><td colspan=2 class="strong-header">New User Registration</td></tr>
595 <tr><td colspan=2><em>marked items</em> are optional...</td></tr>
596 <form action="newuser_action" method=POST>
597 <tr><td align=right><em>Name: </em></td>
598 <td><input name="realname"></td></tr>
599 <tr><td align=right><em>Organisation: </em></td>
600 <td><input name="organisation"></td></tr>
601 <tr><td align=right>E-Mail Address: </td>
602 <td><input name="address"></td></tr>
603 <tr><td align=right><em>Phone: </em></td>
604 <td><input name="phone"></td></tr>
605 <tr><td align=right>Preferred Login name: </td>
606 <td><input name="username"></td></tr>
607 <tr><td align=right>Password: </td>
608 <td><input type="password" name="password"></td></tr>
609 <tr><td align=right>Password Again: </td>
610 <td><input type="password" name="confirm"></td></tr>
611 <tr><td></td>
612 <td><input type="submit" value="Register"></td></tr>
613 </form>
614 </table>
615 ''')
616 self.pagefoot()
618 def login_action(self, message=None):
619 if not self.form.has_key('__login_name'):
620 return self.login(message='Username required')
621 self.user = self.form['__login_name'].value
622 if self.form.has_key('__login_password'):
623 password = self.form['__login_password'].value
624 else:
625 password = ''
626 # make sure the user exists
627 try:
628 uid = self.db.user.lookup(self.user)
629 except KeyError:
630 name = self.user
631 self.make_user_anonymous()
632 return self.login(message='No such user "%s"'%name)
634 # and that the password is correct
635 pw = self.db.user.get(uid, 'password')
636 if password != self.db.user.get(uid, 'password'):
637 self.make_user_anonymous()
638 return self.login(message='Incorrect password')
640 self.set_cookie(self.user, password)
641 return self.index()
643 def set_cookie(self, user, password):
644 # construct the cookie
645 user = binascii.b2a_base64('%s:%s'%(user, password)).strip()
646 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME']))
647 self.header({'Set-Cookie': 'roundup_user="%s"; Path="%s";'%(user,
648 path)})
650 def make_user_anonymous(self):
651 # make us anonymous if we can
652 try:
653 self.db.user.lookup('anonymous')
654 self.user = 'anonymous'
655 except KeyError:
656 self.user = None
658 def logout(self, message=None):
659 self.make_user_anonymous()
660 # construct the logout cookie
661 now = Cookie._getdate()
662 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME']))
663 self.header({'Set-Cookie':
664 'roundup_user=deleted; Max-Age=0; expires="%s"; Path="%s";'%(now,
665 path)})
666 return self.login()
668 def newuser_action(self, message=None):
669 ''' create a new user based on the contents of the form and then
670 set the cookie
671 '''
672 # re-open the database as "admin"
673 self.db.close()
674 self.db = self.instance.open('admin')
676 # TODO: pre-check the required fields and username key property
677 cl = self.db.classes['user']
678 props, dummy = parsePropsFromForm(self.db, cl, self.form)
679 uid = cl.create(**props)
680 self.user = self.db.user.get(uid, 'username')
681 password = self.db.user.get(uid, 'password')
682 self.set_cookie(self.user, password)
683 return self.index()
685 def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
686 nre=re.compile(r'new(\w+)')):
688 # determine the uid to use
689 self.db = self.instance.open('admin')
690 cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
691 user = 'anonymous'
692 if (cookie.has_key('roundup_user') and
693 cookie['roundup_user'].value != 'deleted'):
694 cookie = cookie['roundup_user'].value
695 user, password = binascii.a2b_base64(cookie).split(':')
696 # make sure the user exists
697 try:
698 uid = self.db.user.lookup(user)
699 # now validate the password
700 if password != self.db.user.get(uid, 'password'):
701 user = 'anonymous'
702 except KeyError:
703 user = 'anonymous'
705 # make sure the anonymous user is valid if we're using it
706 if user == 'anonymous':
707 self.make_user_anonymous()
708 else:
709 self.user = user
710 self.db.close()
712 # re-open the database for real, using the user
713 self.db = self.instance.open(self.user)
715 # now figure which function to call
716 path = self.split_path
717 if not path or path[0] in ('', 'index'):
718 action = 'index'
719 else:
720 action = path[0]
722 # Everthing ignores path[1:]
723 # - The file download link generator actually relies on this - it
724 # appends the name of the file to the URL so the download file name
725 # is correct, but doesn't actually use it.
727 # everyone is allowed to try to log in
728 if action == 'login_action':
729 return self.login_action()
731 # allow anonymous people to register
732 if action == 'newuser_action':
733 # if we don't have a login and anonymous people aren't allowed to
734 # register, then spit up the login form
735 if self.ANONYMOUS_REGISTER == 'deny' and self.user is None:
736 return self.login()
737 return self.newuser_action()
739 # make sure totally anonymous access is OK
740 if self.ANONYMOUS_ACCESS == 'deny' and self.user is None:
741 return self.login()
743 # here be the "normal" functionality
744 if action == 'index':
745 return self.index()
746 if action == 'list_classes':
747 return self.classes()
748 if action == 'login':
749 return self.login()
750 if action == 'logout':
751 return self.logout()
752 m = dre.match(action)
753 if m:
754 self.classname = m.group(1)
755 self.nodeid = m.group(2)
756 try:
757 cl = self.db.classes[self.classname]
758 except KeyError:
759 raise NotFound
760 try:
761 cl.get(self.nodeid, 'id')
762 except IndexError:
763 raise NotFound
764 try:
765 func = getattr(self, 'show%s'%self.classname)
766 except AttributeError:
767 raise NotFound
768 return func()
769 m = nre.match(action)
770 if m:
771 self.classname = m.group(1)
772 try:
773 func = getattr(self, 'new%s'%self.classname)
774 except AttributeError:
775 raise NotFound
776 return func()
777 self.classname = action
778 try:
779 self.db.getclass(self.classname)
780 except KeyError:
781 raise NotFound
782 self.list()
784 def __del__(self):
785 self.db.close()
788 class ExtendedClient(Client):
789 '''Includes pages and page heading information that relate to the
790 extended schema.
791 '''
792 showsupport = Client.shownode
793 showtimelog = Client.shownode
794 newsupport = Client.newnode
795 newtimelog = Client.newnode
797 default_index_sort = ['-activity']
798 default_index_group = ['priority']
799 default_index_filter = ['status']
800 default_index_columns = ['activity','status','title','assignedto']
801 default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
803 def pagehead(self, title, message=None):
804 url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
805 machine = self.env['SERVER_NAME']
806 port = self.env['SERVER_PORT']
807 if port != '80': machine = machine + ':' + port
808 base = urlparse.urlunparse(('http', machine, url, None, None, None))
809 if message is not None:
810 message = '<div class="system-msg">%s</div>'%message
811 else:
812 message = ''
813 style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
814 user_name = self.user or ''
815 if self.user == 'admin':
816 admin_links = ' | <a href="list_classes">Class List</a>'
817 else:
818 admin_links = ''
819 if self.user not in (None, 'anonymous'):
820 userid = self.db.user.lookup(self.user)
821 user_info = '''
822 <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> |
823 <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> |
824 <a href="user%s">My Details</a> | <a href="logout">Logout</a>
825 '''%(userid, userid, userid)
826 else:
827 user_info = '<a href="login">Login</a>'
828 if self.user is not None:
829 add_links = '''
830 | Add
831 <a href="newissue">Issue</a>,
832 <a href="newsupport">Support</a>,
833 <a href="newuser">User</a>
834 '''
835 else:
836 add_links = ''
837 self.write('''<html><head>
838 <title>%s</title>
839 <style type="text/css">%s</style>
840 </head>
841 <body bgcolor=#ffffff>
842 %s
843 <table width=100%% border=0 cellspacing=0 cellpadding=2>
844 <tr class="location-bar"><td><big><strong>%s</strong></big></td>
845 <td align=right valign=bottom>%s</td></tr>
846 <tr class="location-bar">
847 <td align=left>All
848 <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>,
849 <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>
850 | Unassigned
851 <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>,
852 <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>
853 %s
854 %s</td>
855 <td align=right>%s</td>
856 </table>
857 '''%(title, style, message, title, user_name, add_links, admin_links,
858 user_info))
860 def parsePropsFromForm(db, cl, form, nodeid=0):
861 '''Pull properties for the given class out of the form.
862 '''
863 props = {}
864 changed = []
865 keys = form.keys()
866 num_re = re.compile('^\d+$')
867 for key in keys:
868 if not cl.properties.has_key(key):
869 continue
870 proptype = cl.properties[key]
871 if isinstance(proptype, hyperdb.String):
872 value = form[key].value.strip()
873 elif isinstance(proptype, hyperdb.Password):
874 value = password.Password(form[key].value.strip())
875 elif isinstance(proptype, hyperdb.Date):
876 value = date.Date(form[key].value.strip())
877 elif isinstance(proptype, hyperdb.Interval):
878 value = date.Interval(form[key].value.strip())
879 elif isinstance(proptype, hyperdb.Link):
880 value = form[key].value.strip()
881 # see if it's the "no selection" choice
882 if value == '-1':
883 # don't set this property
884 continue
885 else:
886 # handle key values
887 link = cl.properties[key].classname
888 if not num_re.match(value):
889 try:
890 value = db.classes[link].lookup(value)
891 except KeyError:
892 raise ValueError, 'property "%s": %s not a %s'%(
893 key, value, link)
894 elif isinstance(proptype, hyperdb.Multilink):
895 value = form[key]
896 if type(value) != type([]):
897 value = [i.strip() for i in value.value.split(',')]
898 else:
899 value = [i.value.strip() for i in value]
900 link = cl.properties[key].classname
901 l = []
902 for entry in map(str, value):
903 if not num_re.match(entry):
904 try:
905 entry = db.classes[link].lookup(entry)
906 except KeyError:
907 raise ValueError, \
908 'property "%s": "%s" not an entry of %s'%(key,
909 entry, link.capitalize())
910 l.append(entry)
911 l.sort()
912 value = l
913 props[key] = value
914 # if changed, set it
915 if nodeid and value != cl.get(nodeid, key):
916 changed.append(key)
917 props[key] = value
918 return props, changed
920 #
921 # $Log: not supported by cvs2svn $
922 # Revision 1.48 2001/11/03 01:30:18 richard
923 # Oops. uses pagefoot now.
924 #
925 # Revision 1.47 2001/11/03 01:29:28 richard
926 # Login page didn't have all close tags.
927 #
928 # Revision 1.46 2001/11/03 01:26:55 richard
929 # possibly fix truncated base64'ed user:pass
930 #
931 # Revision 1.45 2001/11/01 22:04:37 richard
932 # Started work on supporting a pop3-fetching server
933 # Fixed bugs:
934 # . bug #477104 ] HTML tag error in roundup-server
935 # . bug #477107 ] HTTP header problem
936 #
937 # Revision 1.44 2001/10/28 23:03:08 richard
938 # Added more useful header to the classic schema.
939 #
940 # Revision 1.43 2001/10/24 00:01:42 richard
941 # More fixes to lockout logic.
942 #
943 # Revision 1.42 2001/10/23 23:56:03 richard
944 # HTML typo
945 #
946 # Revision 1.41 2001/10/23 23:52:35 richard
947 # Fixed lock-out logic, thanks Roch'e for pointing out the problems.
948 #
949 # Revision 1.40 2001/10/23 23:06:39 richard
950 # Some cleanup.
951 #
952 # Revision 1.39 2001/10/23 01:00:18 richard
953 # Re-enabled login and registration access after lopping them off via
954 # disabling access for anonymous users.
955 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
956 # a couple of bugs while I was there. Probably introduced a couple, but
957 # things seem to work OK at the moment.
958 #
959 # Revision 1.38 2001/10/22 03:25:01 richard
960 # Added configuration for:
961 # . anonymous user access and registration (deny/allow)
962 # . filter "widget" location on index page (top, bottom, both)
963 # Updated some documentation.
964 #
965 # Revision 1.37 2001/10/21 07:26:35 richard
966 # feature #473127: Filenames. I modified the file.index and htmltemplate
967 # source so that the filename is used in the link and the creation
968 # information is displayed.
969 #
970 # Revision 1.36 2001/10/21 04:44:50 richard
971 # bug #473124: UI inconsistency with Link fields.
972 # This also prompted me to fix a fairly long-standing usability issue -
973 # that of being able to turn off certain filters.
974 #
975 # Revision 1.35 2001/10/21 00:17:54 richard
976 # CGI interface view customisation section may now be hidden (patch from
977 # Roch'e Compaan.)
978 #
979 # Revision 1.34 2001/10/20 11:58:48 richard
980 # Catch errors in login - no username or password supplied.
981 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
982 #
983 # Revision 1.33 2001/10/17 00:18:41 richard
984 # Manually constructing cookie headers now.
985 #
986 # Revision 1.32 2001/10/16 03:36:21 richard
987 # CGI interface wasn't handling checkboxes at all.
988 #
989 # Revision 1.31 2001/10/14 10:55:00 richard
990 # Handle empty strings in HTML template Link function
991 #
992 # Revision 1.30 2001/10/09 07:38:58 richard
993 # Pushed the base code for the extended schema CGI interface back into the
994 # code cgi_client module so that future updates will be less painful.
995 # Also removed a debugging print statement from cgi_client.
996 #
997 # Revision 1.29 2001/10/09 07:25:59 richard
998 # Added the Password property type. See "pydoc roundup.password" for
999 # implementation details. Have updated some of the documentation too.
1000 #
1001 # Revision 1.28 2001/10/08 00:34:31 richard
1002 # Change message was stuffing up for multilinks with no key property.
1003 #
1004 # Revision 1.27 2001/10/05 02:23:24 richard
1005 # . roundup-admin create now prompts for property info if none is supplied
1006 # on the command-line.
1007 # . hyperdb Class getprops() method may now return only the mutable
1008 # properties.
1009 # . Login now uses cookies, which makes it a whole lot more flexible. We can
1010 # now support anonymous user access (read-only, unless there's an
1011 # "anonymous" user, in which case write access is permitted). Login
1012 # handling has been moved into cgi_client.Client.main()
1013 # . The "extended" schema is now the default in roundup init.
1014 # . The schemas have had their page headings modified to cope with the new
1015 # login handling. Existing installations should copy the interfaces.py
1016 # file from the roundup lib directory to their instance home.
1017 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
1018 # Ping - has been removed.
1019 # . Fixed a whole bunch of places in the CGI interface where we should have
1020 # been returning Not Found instead of throwing an exception.
1021 # . Fixed a deviation from the spec: trying to modify the 'id' property of
1022 # an item now throws an exception.
1023 #
1024 # Revision 1.26 2001/09/12 08:31:42 richard
1025 # handle cases where mime type is not guessable
1026 #
1027 # Revision 1.25 2001/08/29 05:30:49 richard
1028 # change messages weren't being saved when there was no-one on the nosy list.
1029 #
1030 # Revision 1.24 2001/08/29 04:49:39 richard
1031 # didn't clean up fully after debugging :(
1032 #
1033 # Revision 1.23 2001/08/29 04:47:18 richard
1034 # Fixed CGI client change messages so they actually include the properties
1035 # changed (again).
1036 #
1037 # Revision 1.22 2001/08/17 00:08:10 richard
1038 # reverted back to sending messages always regardless of who is doing the web
1039 # edit. change notes weren't being saved. bleah. hackish.
1040 #
1041 # Revision 1.21 2001/08/15 23:43:18 richard
1042 # Fixed some isFooTypes that I missed.
1043 # Refactored some code in the CGI code.
1044 #
1045 # Revision 1.20 2001/08/12 06:32:36 richard
1046 # using isinstance(blah, Foo) now instead of isFooType
1047 #
1048 # Revision 1.19 2001/08/07 00:24:42 richard
1049 # stupid typo
1050 #
1051 # Revision 1.18 2001/08/07 00:15:51 richard
1052 # Added the copyright/license notice to (nearly) all files at request of
1053 # Bizar Software.
1054 #
1055 # Revision 1.17 2001/08/02 06:38:17 richard
1056 # Roundupdb now appends "mailing list" information to its messages which
1057 # include the e-mail address and web interface address. Templates may
1058 # override this in their db classes to include specific information (support
1059 # instructions, etc).
1060 #
1061 # Revision 1.16 2001/08/02 05:55:25 richard
1062 # Web edit messages aren't sent to the person who did the edit any more. No
1063 # message is generated if they are the only person on the nosy list.
1064 #
1065 # Revision 1.15 2001/08/02 00:34:10 richard
1066 # bleah syntax error
1067 #
1068 # Revision 1.14 2001/08/02 00:26:16 richard
1069 # Changed the order of the information in the message generated by web edits.
1070 #
1071 # Revision 1.13 2001/07/30 08:12:17 richard
1072 # Added time logging and file uploading to the templates.
1073 #
1074 # Revision 1.12 2001/07/30 06:26:31 richard
1075 # Added some documentation on how the newblah works.
1076 #
1077 # Revision 1.11 2001/07/30 06:17:45 richard
1078 # Features:
1079 # . Added ability for cgi newblah forms to indicate that the new node
1080 # should be linked somewhere.
1081 # Fixed:
1082 # . Fixed the agument handling for the roundup-admin find command.
1083 # . Fixed handling of summary when no note supplied for newblah. Again.
1084 # . Fixed detection of no form in htmltemplate Field display.
1085 #
1086 # Revision 1.10 2001/07/30 02:37:34 richard
1087 # Temporary measure until we have decent schema migration...
1088 #
1089 # Revision 1.9 2001/07/30 01:25:07 richard
1090 # Default implementation is now "classic" rather than "extended" as one would
1091 # expect.
1092 #
1093 # Revision 1.8 2001/07/29 08:27:40 richard
1094 # Fixed handling of passed-in values in form elements (ie. during a
1095 # drill-down)
1096 #
1097 # Revision 1.7 2001/07/29 07:01:39 richard
1098 # Added vim command to all source so that we don't get no steenkin' tabs :)
1099 #
1100 # Revision 1.6 2001/07/29 04:04:00 richard
1101 # Moved some code around allowing for subclassing to change behaviour.
1102 #
1103 # Revision 1.5 2001/07/28 08:16:52 richard
1104 # New issue form handles lack of note better now.
1105 #
1106 # Revision 1.4 2001/07/28 00:34:34 richard
1107 # Fixed some non-string node ids.
1108 #
1109 # Revision 1.3 2001/07/23 03:56:30 richard
1110 # oops, missed a config removal
1111 #
1112 # Revision 1.2 2001/07/22 12:09:32 richard
1113 # Final commit of Grande Splite
1114 #
1115 # Revision 1.1 2001/07/22 11:58:35 richard
1116 # More Grande Splite
1117 #
1118 #
1119 # vim: set filetype=python ts=4 sw=4 et si