b17745ce97395569970376bb2ee9d5545f0fe8f2
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.48 2001-11-03 01:30:18 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 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 self.pagefoot()
546 return
547 self.write('''
548 <p>
549 <tr><td colspan=2 class="strong-header">New User Registration</td></tr>
550 <tr><td colspan=2><em>marked items</em> are optional...</td></tr>
551 <form action="newuser_action" method=POST>
552 <tr><td align=right><em>Name: </em></td>
553 <td><input name="realname"></td></tr>
554 <tr><td align=right><em>Organisation: </em></td>
555 <td><input name="organisation"></td></tr>
556 <tr><td align=right>E-Mail Address: </td>
557 <td><input name="address"></td></tr>
558 <tr><td align=right><em>Phone: </em></td>
559 <td><input name="phone"></td></tr>
560 <tr><td align=right>Preferred Login name: </td>
561 <td><input name="username"></td></tr>
562 <tr><td align=right>Password: </td>
563 <td><input type="password" name="password"></td></tr>
564 <tr><td align=right>Password Again: </td>
565 <td><input type="password" name="confirm"></td></tr>
566 <tr><td></td>
567 <td><input type="submit" value="Register"></td></tr>
568 </form>
569 </table>
570 ''')
571 self.pagefoot()
573 def login_action(self, message=None):
574 if not self.form.has_key('__login_name'):
575 return self.login(message='Username required')
576 self.user = self.form['__login_name'].value
577 if self.form.has_key('__login_password'):
578 password = self.form['__login_password'].value
579 else:
580 password = ''
581 print self.user, password
582 # make sure the user exists
583 try:
584 uid = self.db.user.lookup(self.user)
585 except KeyError:
586 name = self.user
587 self.make_user_anonymous()
588 return self.login(message='No such user "%s"'%name)
590 # and that the password is correct
591 pw = self.db.user.get(uid, 'password')
592 if password != self.db.user.get(uid, 'password'):
593 self.make_user_anonymous()
594 return self.login(message='Incorrect password')
596 # construct the cookie
597 uid = self.db.user.lookup(self.user)
598 user = binascii.b2a_base64('%s:%s'%(self.user, password)).strip()
599 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
600 ''))
601 self.header({'Set-Cookie': 'roundup_user=%s; Path=%s;'%(user, path)})
602 return self.index()
604 def make_user_anonymous(self):
605 # make us anonymous if we can
606 try:
607 self.db.user.lookup('anonymous')
608 self.user = 'anonymous'
609 except KeyError:
610 self.user = None
612 def logout(self, message=None):
613 self.make_user_anonymous()
614 # construct the logout cookie
615 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
616 ''))
617 now = Cookie._getdate()
618 self.header({'Set-Cookie':
619 'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
620 return self.login()
622 def newuser_action(self, message=None):
623 ''' create a new user based on the contents of the form and then
624 set the cookie
625 '''
626 # re-open the database as "admin"
627 self.db.close()
628 self.db = self.instance.open('admin')
630 # TODO: pre-check the required fields and username key property
631 cl = self.db.classes['user']
632 props, dummy = parsePropsFromForm(self.db, cl, self.form)
633 uid = cl.create(**props)
634 self.user = self.db.user.get(uid, 'username')
635 password = self.db.user.get(uid, 'password')
636 # construct the cookie
637 uid = self.db.user.lookup(self.user)
638 user = binascii.b2a_base64('%s:%s'%(self.user, password)).strip()
639 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
640 ''))
641 self.header({'Set-Cookie': 'roundup_user=%s; Path=%s;'%(user, path)})
642 return self.index()
644 def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
645 nre=re.compile(r'new(\w+)')):
647 # determine the uid to use
648 self.db = self.instance.open('admin')
649 cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
650 user = 'anonymous'
651 if (cookie.has_key('roundup_user') and
652 cookie['roundup_user'].value != 'deleted'):
653 cookie = cookie['roundup_user'].value
654 user, password = binascii.a2b_base64(cookie).split(':')
655 # make sure the user exists
656 try:
657 uid = self.db.user.lookup(user)
658 # now validate the password
659 if password != self.db.user.get(uid, 'password'):
660 user = 'anonymous'
661 except KeyError:
662 user = 'anonymous'
664 # make sure the anonymous user is valid if we're using it
665 if user == 'anonymous':
666 self.make_user_anonymous()
667 else:
668 self.user = user
669 self.db.close()
671 # re-open the database for real, using the user
672 self.db = self.instance.open(self.user)
674 # now figure which function to call
675 path = self.split_path
676 if not path or path[0] in ('', 'index'):
677 action = 'index'
678 else:
679 action = path[0]
681 # Everthing ignores path[1:]
682 # - The file download link generator actually relies on this - it
683 # appends the name of the file to the URL so the download file name
684 # is correct, but doesn't actually use it.
686 # everyone is allowed to try to log in
687 if action == 'login_action':
688 return self.login_action()
690 # allow anonymous people to register
691 if action == 'newuser_action':
692 # if we don't have a login and anonymous people aren't allowed to
693 # register, then spit up the login form
694 if self.ANONYMOUS_REGISTER == 'deny' and self.user is None:
695 return self.login()
696 return self.newuser_action()
698 # make sure totally anonymous access is OK
699 if self.ANONYMOUS_ACCESS == 'deny' and self.user is None:
700 return self.login()
702 # here be the "normal" functionality
703 if action == 'index':
704 return self.index()
705 if action == 'list_classes':
706 return self.classes()
707 if action == 'login':
708 return self.login()
709 if action == 'logout':
710 return self.logout()
711 m = dre.match(action)
712 if m:
713 self.classname = m.group(1)
714 self.nodeid = m.group(2)
715 try:
716 cl = self.db.classes[self.classname]
717 except KeyError:
718 raise NotFound
719 try:
720 cl.get(self.nodeid, 'id')
721 except IndexError:
722 raise NotFound
723 try:
724 func = getattr(self, 'show%s'%self.classname)
725 except AttributeError:
726 raise NotFound
727 return func()
728 m = nre.match(action)
729 if m:
730 self.classname = m.group(1)
731 try:
732 func = getattr(self, 'new%s'%self.classname)
733 except AttributeError:
734 raise NotFound
735 return func()
736 self.classname = action
737 try:
738 self.db.getclass(self.classname)
739 except KeyError:
740 raise NotFound
741 self.list()
743 def __del__(self):
744 self.db.close()
747 class ExtendedClient(Client):
748 '''Includes pages and page heading information that relate to the
749 extended schema.
750 '''
751 showsupport = Client.shownode
752 showtimelog = Client.shownode
753 newsupport = Client.newnode
754 newtimelog = Client.newnode
756 default_index_sort = ['-activity']
757 default_index_group = ['priority']
758 default_index_filter = ['status']
759 default_index_columns = ['activity','status','title','assignedto']
760 default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
762 def pagehead(self, title, message=None):
763 url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
764 machine = self.env['SERVER_NAME']
765 port = self.env['SERVER_PORT']
766 if port != '80': machine = machine + ':' + port
767 base = urlparse.urlunparse(('http', machine, url, None, None, None))
768 if message is not None:
769 message = '<div class="system-msg">%s</div>'%message
770 else:
771 message = ''
772 style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
773 user_name = self.user or ''
774 if self.user == 'admin':
775 admin_links = ' | <a href="list_classes">Class List</a>'
776 else:
777 admin_links = ''
778 if self.user not in (None, 'anonymous'):
779 userid = self.db.user.lookup(self.user)
780 user_info = '''
781 <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> |
782 <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> |
783 <a href="user%s">My Details</a> | <a href="logout">Logout</a>
784 '''%(userid, userid, userid)
785 else:
786 user_info = '<a href="login">Login</a>'
787 if self.user is not None:
788 add_links = '''
789 | Add
790 <a href="newissue">Issue</a>,
791 <a href="newsupport">Support</a>,
792 <a href="newuser">User</a>
793 '''
794 else:
795 add_links = ''
796 self.write('''<html><head>
797 <title>%s</title>
798 <style type="text/css">%s</style>
799 </head>
800 <body bgcolor=#ffffff>
801 %s
802 <table width=100%% border=0 cellspacing=0 cellpadding=2>
803 <tr class="location-bar"><td><big><strong>%s</strong></big></td>
804 <td align=right valign=bottom>%s</td></tr>
805 <tr class="location-bar">
806 <td align=left>All
807 <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>,
808 <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>
809 | Unassigned
810 <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>,
811 <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>
812 %s
813 %s</td>
814 <td align=right>%s</td>
815 </table>
816 '''%(title, style, message, title, user_name, add_links, admin_links,
817 user_info))
819 def parsePropsFromForm(db, cl, form, nodeid=0):
820 '''Pull properties for the given class out of the form.
821 '''
822 props = {}
823 changed = []
824 keys = form.keys()
825 num_re = re.compile('^\d+$')
826 for key in keys:
827 if not cl.properties.has_key(key):
828 continue
829 proptype = cl.properties[key]
830 if isinstance(proptype, hyperdb.String):
831 value = form[key].value.strip()
832 elif isinstance(proptype, hyperdb.Password):
833 value = password.Password(form[key].value.strip())
834 elif isinstance(proptype, hyperdb.Date):
835 value = date.Date(form[key].value.strip())
836 elif isinstance(proptype, hyperdb.Interval):
837 value = date.Interval(form[key].value.strip())
838 elif isinstance(proptype, hyperdb.Link):
839 value = form[key].value.strip()
840 # see if it's the "no selection" choice
841 if value == '-1':
842 # don't set this property
843 continue
844 else:
845 # handle key values
846 link = cl.properties[key].classname
847 if not num_re.match(value):
848 try:
849 value = db.classes[link].lookup(value)
850 except KeyError:
851 raise ValueError, 'property "%s": %s not a %s'%(
852 key, value, link)
853 elif isinstance(proptype, hyperdb.Multilink):
854 value = form[key]
855 if type(value) != type([]):
856 value = [i.strip() for i in value.value.split(',')]
857 else:
858 value = [i.value.strip() for i in value]
859 link = cl.properties[key].classname
860 l = []
861 for entry in map(str, value):
862 if not num_re.match(entry):
863 try:
864 entry = db.classes[link].lookup(entry)
865 except KeyError:
866 raise ValueError, \
867 'property "%s": "%s" not an entry of %s'%(key,
868 entry, link.capitalize())
869 l.append(entry)
870 l.sort()
871 value = l
872 props[key] = value
873 # if changed, set it
874 if nodeid and value != cl.get(nodeid, key):
875 changed.append(key)
876 props[key] = value
877 return props, changed
879 #
880 # $Log: not supported by cvs2svn $
881 # Revision 1.47 2001/11/03 01:29:28 richard
882 # Login page didn't have all close tags.
883 #
884 # Revision 1.46 2001/11/03 01:26:55 richard
885 # possibly fix truncated base64'ed user:pass
886 #
887 # Revision 1.45 2001/11/01 22:04:37 richard
888 # Started work on supporting a pop3-fetching server
889 # Fixed bugs:
890 # . bug #477104 ] HTML tag error in roundup-server
891 # . bug #477107 ] HTTP header problem
892 #
893 # Revision 1.44 2001/10/28 23:03:08 richard
894 # Added more useful header to the classic schema.
895 #
896 # Revision 1.43 2001/10/24 00:01:42 richard
897 # More fixes to lockout logic.
898 #
899 # Revision 1.42 2001/10/23 23:56:03 richard
900 # HTML typo
901 #
902 # Revision 1.41 2001/10/23 23:52:35 richard
903 # Fixed lock-out logic, thanks Roch'e for pointing out the problems.
904 #
905 # Revision 1.40 2001/10/23 23:06:39 richard
906 # Some cleanup.
907 #
908 # Revision 1.39 2001/10/23 01:00:18 richard
909 # Re-enabled login and registration access after lopping them off via
910 # disabling access for anonymous users.
911 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
912 # a couple of bugs while I was there. Probably introduced a couple, but
913 # things seem to work OK at the moment.
914 #
915 # Revision 1.38 2001/10/22 03:25:01 richard
916 # Added configuration for:
917 # . anonymous user access and registration (deny/allow)
918 # . filter "widget" location on index page (top, bottom, both)
919 # Updated some documentation.
920 #
921 # Revision 1.37 2001/10/21 07:26:35 richard
922 # feature #473127: Filenames. I modified the file.index and htmltemplate
923 # source so that the filename is used in the link and the creation
924 # information is displayed.
925 #
926 # Revision 1.36 2001/10/21 04:44:50 richard
927 # bug #473124: UI inconsistency with Link fields.
928 # This also prompted me to fix a fairly long-standing usability issue -
929 # that of being able to turn off certain filters.
930 #
931 # Revision 1.35 2001/10/21 00:17:54 richard
932 # CGI interface view customisation section may now be hidden (patch from
933 # Roch'e Compaan.)
934 #
935 # Revision 1.34 2001/10/20 11:58:48 richard
936 # Catch errors in login - no username or password supplied.
937 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
938 #
939 # Revision 1.33 2001/10/17 00:18:41 richard
940 # Manually constructing cookie headers now.
941 #
942 # Revision 1.32 2001/10/16 03:36:21 richard
943 # CGI interface wasn't handling checkboxes at all.
944 #
945 # Revision 1.31 2001/10/14 10:55:00 richard
946 # Handle empty strings in HTML template Link function
947 #
948 # Revision 1.30 2001/10/09 07:38:58 richard
949 # Pushed the base code for the extended schema CGI interface back into the
950 # code cgi_client module so that future updates will be less painful.
951 # Also removed a debugging print statement from cgi_client.
952 #
953 # Revision 1.29 2001/10/09 07:25:59 richard
954 # Added the Password property type. See "pydoc roundup.password" for
955 # implementation details. Have updated some of the documentation too.
956 #
957 # Revision 1.28 2001/10/08 00:34:31 richard
958 # Change message was stuffing up for multilinks with no key property.
959 #
960 # Revision 1.27 2001/10/05 02:23:24 richard
961 # . roundup-admin create now prompts for property info if none is supplied
962 # on the command-line.
963 # . hyperdb Class getprops() method may now return only the mutable
964 # properties.
965 # . Login now uses cookies, which makes it a whole lot more flexible. We can
966 # now support anonymous user access (read-only, unless there's an
967 # "anonymous" user, in which case write access is permitted). Login
968 # handling has been moved into cgi_client.Client.main()
969 # . The "extended" schema is now the default in roundup init.
970 # . The schemas have had their page headings modified to cope with the new
971 # login handling. Existing installations should copy the interfaces.py
972 # file from the roundup lib directory to their instance home.
973 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
974 # Ping - has been removed.
975 # . Fixed a whole bunch of places in the CGI interface where we should have
976 # been returning Not Found instead of throwing an exception.
977 # . Fixed a deviation from the spec: trying to modify the 'id' property of
978 # an item now throws an exception.
979 #
980 # Revision 1.26 2001/09/12 08:31:42 richard
981 # handle cases where mime type is not guessable
982 #
983 # Revision 1.25 2001/08/29 05:30:49 richard
984 # change messages weren't being saved when there was no-one on the nosy list.
985 #
986 # Revision 1.24 2001/08/29 04:49:39 richard
987 # didn't clean up fully after debugging :(
988 #
989 # Revision 1.23 2001/08/29 04:47:18 richard
990 # Fixed CGI client change messages so they actually include the properties
991 # changed (again).
992 #
993 # Revision 1.22 2001/08/17 00:08:10 richard
994 # reverted back to sending messages always regardless of who is doing the web
995 # edit. change notes weren't being saved. bleah. hackish.
996 #
997 # Revision 1.21 2001/08/15 23:43:18 richard
998 # Fixed some isFooTypes that I missed.
999 # Refactored some code in the CGI code.
1000 #
1001 # Revision 1.20 2001/08/12 06:32:36 richard
1002 # using isinstance(blah, Foo) now instead of isFooType
1003 #
1004 # Revision 1.19 2001/08/07 00:24:42 richard
1005 # stupid typo
1006 #
1007 # Revision 1.18 2001/08/07 00:15:51 richard
1008 # Added the copyright/license notice to (nearly) all files at request of
1009 # Bizar Software.
1010 #
1011 # Revision 1.17 2001/08/02 06:38:17 richard
1012 # Roundupdb now appends "mailing list" information to its messages which
1013 # include the e-mail address and web interface address. Templates may
1014 # override this in their db classes to include specific information (support
1015 # instructions, etc).
1016 #
1017 # Revision 1.16 2001/08/02 05:55:25 richard
1018 # Web edit messages aren't sent to the person who did the edit any more. No
1019 # message is generated if they are the only person on the nosy list.
1020 #
1021 # Revision 1.15 2001/08/02 00:34:10 richard
1022 # bleah syntax error
1023 #
1024 # Revision 1.14 2001/08/02 00:26:16 richard
1025 # Changed the order of the information in the message generated by web edits.
1026 #
1027 # Revision 1.13 2001/07/30 08:12:17 richard
1028 # Added time logging and file uploading to the templates.
1029 #
1030 # Revision 1.12 2001/07/30 06:26:31 richard
1031 # Added some documentation on how the newblah works.
1032 #
1033 # Revision 1.11 2001/07/30 06:17:45 richard
1034 # Features:
1035 # . Added ability for cgi newblah forms to indicate that the new node
1036 # should be linked somewhere.
1037 # Fixed:
1038 # . Fixed the agument handling for the roundup-admin find command.
1039 # . Fixed handling of summary when no note supplied for newblah. Again.
1040 # . Fixed detection of no form in htmltemplate Field display.
1041 #
1042 # Revision 1.10 2001/07/30 02:37:34 richard
1043 # Temporary measure until we have decent schema migration...
1044 #
1045 # Revision 1.9 2001/07/30 01:25:07 richard
1046 # Default implementation is now "classic" rather than "extended" as one would
1047 # expect.
1048 #
1049 # Revision 1.8 2001/07/29 08:27:40 richard
1050 # Fixed handling of passed-in values in form elements (ie. during a
1051 # drill-down)
1052 #
1053 # Revision 1.7 2001/07/29 07:01:39 richard
1054 # Added vim command to all source so that we don't get no steenkin' tabs :)
1055 #
1056 # Revision 1.6 2001/07/29 04:04:00 richard
1057 # Moved some code around allowing for subclassing to change behaviour.
1058 #
1059 # Revision 1.5 2001/07/28 08:16:52 richard
1060 # New issue form handles lack of note better now.
1061 #
1062 # Revision 1.4 2001/07/28 00:34:34 richard
1063 # Fixed some non-string node ids.
1064 #
1065 # Revision 1.3 2001/07/23 03:56:30 richard
1066 # oops, missed a config removal
1067 #
1068 # Revision 1.2 2001/07/22 12:09:32 richard
1069 # Final commit of Grande Splite
1070 #
1071 # Revision 1.1 2001/07/22 11:58:35 richard
1072 # More Grande Splite
1073 #
1074 #
1075 # vim: set filetype=python ts=4 sw=4 et si