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