d4cdaf5891a4cd802acf8ba8968a44d9bf87e1cd
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.30 2001-10-09 07:38:58 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(cl, self.form, self.nodeid)
222 cl.set(self.nodeid, **props)
223 self._post_editnode(self.nodeid, changed)
224 # and some nice feedback for the user
225 message = '%s edited ok'%', '.join(changed)
226 except:
227 s = StringIO.StringIO()
228 traceback.print_exc(None, s)
229 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
231 # now the display
232 id = self.nodeid
233 if cl.getkey():
234 id = cl.get(id, cl.getkey())
235 self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
237 nodeid = self.nodeid
239 # use the template to display the item
240 htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid)
241 self.pagefoot()
242 showissue = shownode
243 showmsg = shownode
245 def showuser(self, message=None):
246 ''' display an item
247 '''
248 if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
249 self.shownode(message)
250 else:
251 raise Unauthorised
253 def showfile(self):
254 ''' display a file
255 '''
256 nodeid = self.nodeid
257 cl = self.db.file
258 type = cl.get(nodeid, 'type')
259 if type == 'message/rfc822':
260 type = 'text/plain'
261 self.header(headers={'Content-Type': type})
262 self.write(cl.get(nodeid, 'content'))
264 def _createnode(self):
265 ''' create a node based on the contents of the form
266 '''
267 cl = self.db.classes[self.classname]
268 props, dummy = parsePropsFromForm(cl, self.form)
269 return cl.create(**props)
271 def _post_editnode(self, nid, changes=None):
272 ''' do the linking and message sending part of the node creation
273 '''
274 cn = self.classname
275 cl = self.db.classes[cn]
276 # link if necessary
277 keys = self.form.keys()
278 for key in keys:
279 if key == ':multilink':
280 value = self.form[key].value
281 if type(value) != type([]): value = [value]
282 for value in value:
283 designator, property = value.split(':')
284 link, nodeid = roundupdb.splitDesignator(designator)
285 link = self.db.classes[link]
286 value = link.get(nodeid, property)
287 value.append(nid)
288 link.set(nodeid, **{property: value})
289 elif key == ':link':
290 value = self.form[key].value
291 if type(value) != type([]): value = [value]
292 for value in value:
293 designator, property = value.split(':')
294 link, nodeid = roundupdb.splitDesignator(designator)
295 link = self.db.classes[link]
296 link.set(nodeid, **{property: nid})
298 # generate an edit message
299 # don't bother if there's no messages or nosy list
300 props = cl.getprops()
301 note = None
302 if self.form.has_key('__note'):
303 note = self.form['__note']
304 note = note.value
305 send = len(cl.get(nid, 'nosy', [])) or note
306 if (send and props.has_key('messages') and
307 isinstance(props['messages'], hyperdb.Multilink) and
308 props['messages'].classname == 'msg'):
310 # handle the note
311 if note:
312 if '\n' in note:
313 summary = re.split(r'\n\r?', note)[0]
314 else:
315 summary = note
316 m = ['%s\n'%note]
317 else:
318 summary = 'This %s has been edited through the web.\n'%cn
319 m = [summary]
321 first = 1
322 for name, prop in props.items():
323 if changes is not None and name not in changes: continue
324 if first:
325 m.append('\n-------')
326 first = 0
327 value = cl.get(nid, name, None)
328 if isinstance(prop, hyperdb.Link):
329 link = self.db.classes[prop.classname]
330 key = link.labelprop(default_to_id=1)
331 if value is not None and key:
332 value = link.get(value, key)
333 else:
334 value = '-'
335 elif isinstance(prop, hyperdb.Multilink):
336 if value is None: value = []
337 l = []
338 link = self.db.classes[prop.classname]
339 key = link.labelprop(default_to_id=1)
340 for entry in value:
341 if key:
342 l.append(link.get(entry, key))
343 else:
344 l.append(entry)
345 value = ', '.join(l)
346 m.append('%s: %s'%(name, value))
348 # now create the message
349 content = '\n'.join(m)
350 message_id = self.db.msg.create(author=self.getuid(),
351 recipients=[], date=date.Date('.'), summary=summary,
352 content=content)
353 messages = cl.get(nid, 'messages')
354 messages.append(message_id)
355 props = {'messages': messages}
356 cl.set(nid, **props)
358 def newnode(self, message=None):
359 ''' Add a new node to the database.
361 The form works in two modes: blank form and submission (that is,
362 the submission goes to the same URL). **Eventually this means that
363 the form will have previously entered information in it if
364 submission fails.
366 The new node will be created with the properties specified in the
367 form submission. For multilinks, multiple form entries are handled,
368 as are prop=value,value,value. You can't mix them though.
370 If the new node is to be referenced from somewhere else immediately
371 (ie. the new node is a file that is to be attached to a support
372 issue) then supply one of these arguments in addition to the usual
373 form entries:
374 :link=designator:property
375 :multilink=designator:property
376 ... which means that once the new node is created, the "property"
377 on the node given by "designator" should now reference the new
378 node's id. The node id will be appended to the multilink.
379 '''
380 cn = self.classname
381 cl = self.db.classes[cn]
383 # possibly perform a create
384 keys = self.form.keys()
385 if [i for i in keys if i[0] != ':']:
386 props = {}
387 try:
388 nid = self._createnode()
389 self._post_editnode(nid)
390 # and some nice feedback for the user
391 message = '%s created ok'%cn
392 except:
393 s = StringIO.StringIO()
394 traceback.print_exc(None, s)
395 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
396 self.pagehead('New %s'%self.classname.capitalize(), message)
397 htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
398 self.form)
399 self.pagefoot()
400 newissue = newnode
401 newuser = newnode
403 def newfile(self, message=None):
404 ''' Add a new file to the database.
406 This form works very much the same way as newnode - it just has a
407 file upload.
408 '''
409 cn = self.classname
410 cl = self.db.classes[cn]
412 # possibly perform a create
413 keys = self.form.keys()
414 if [i for i in keys if i[0] != ':']:
415 try:
416 file = self.form['content']
417 type = mimetypes.guess_type(file.filename)[0]
418 if not type:
419 type = "application/octet-stream"
420 self._post_editnode(cl.create(content=file.file.read(),
421 type=type, name=file.filename))
422 # and some nice feedback for the user
423 message = '%s created ok'%cn
424 except:
425 s = StringIO.StringIO()
426 traceback.print_exc(None, s)
427 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
429 self.pagehead('New %s'%self.classname.capitalize(), message)
430 htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
431 self.form)
432 self.pagefoot()
434 def classes(self, message=None):
435 ''' display a list of all the classes in the database
436 '''
437 if self.user == 'admin':
438 self.pagehead('Table of classes', message)
439 classnames = self.db.classes.keys()
440 classnames.sort()
441 self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
442 for cn in classnames:
443 cl = self.db.getclass(cn)
444 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
445 for key, value in cl.properties.items():
446 if value is None: value = ''
447 else: value = str(value)
448 self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
449 key, cgi.escape(value)))
450 self.write('</table>')
451 self.pagefoot()
452 else:
453 raise Unauthorised
455 def login(self, message=None):
456 self.pagehead('Login to roundup', message)
457 self.write('''
458 <table>
459 <tr><td colspan=2 class="strong-header">Existing User Login</td></tr>
460 <form action="login_action" method=POST>
461 <tr><td align=right>Login name: </td>
462 <td><input name="__login_name"></td></tr>
463 <tr><td align=right>Password: </td>
464 <td><input type="password" name="__login_password"></td></tr>
465 <tr><td></td>
466 <td><input type="submit" value="Log In"></td></tr>
467 </form>
469 <p>
470 <tr><td colspan=2 class="strong-header">New User Registration</td></tr>
471 <tr><td colspan=2><em>marked items</em> are optional...</td></tr>
472 <form action="newuser_action" method=POST>
473 <tr><td align=right><em>Name: </em></td>
474 <td><input name="__newuser_realname"></td></tr>
475 <tr><td align=right><em>Organisation: </em></td>
476 <td><input name="__newuser_organisation"></td></tr>
477 <tr><td align=right>E-Mail Address: </td>
478 <td><input name="__newuser_address"></td></tr>
479 <tr><td align=right><em>Phone: </em></td>
480 <td><input name="__newuser_phone"></td></tr>
481 <tr><td align=right>Preferred Login name: </td>
482 <td><input name="__newuser_username"></td></tr>
483 <tr><td align=right>Password: </td>
484 <td><input type="password" name="__newuser_password"></td></tr>
485 <tr><td align=right>Password Again: </td>
486 <td><input type="password" name="__newuser_confirm"></td></tr>
487 <tr><td></td>
488 <td><input type="submit" value="Register"></td></tr>
489 </form>
490 </table>
491 ''')
493 def login_action(self, message=None):
494 self.user = self.form['__login_name'].value
495 password = self.form['__login_password'].value
496 # make sure the user exists
497 try:
498 uid = self.db.user.lookup(self.user)
499 except KeyError:
500 name = self.user
501 self.make_user_anonymous()
502 return self.login(message='No such user "%s"'%name)
504 # and that the password is correct
505 pw = self.db.user.get(uid, 'password')
506 if password != self.db.user.get(uid, 'password'):
507 self.make_user_anonymous()
508 return self.login(message='Incorrect password')
510 # construct the cookie
511 uid = self.db.user.lookup(self.user)
512 user = base64.encodestring('%s:%s'%(self.user, password))[:-1]
513 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
514 ''))
515 cookie = Cookie.SmartCookie()
516 cookie['roundup_user'] = user
517 cookie['roundup_user']['path'] = path
518 self.header({'Set-Cookie': str(cookie)})
519 return self.index()
521 def make_user_anonymous(self):
522 # make us anonymous if we can
523 try:
524 self.db.user.lookup('anonymous')
525 self.user = 'anonymous'
526 except KeyError:
527 self.user = None
529 def logout(self, message=None):
530 self.make_user_anonymous()
531 # construct the logout cookie
532 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
533 ''))
534 cookie = Cookie.SmartCookie()
535 cookie['roundup_user'] = 'deleted'
536 cookie['roundup_user']['path'] = path
537 cookie['roundup_user']['expires'] = 0
538 cookie['roundup_user']['max-age'] = 0
539 self.header({'Set-Cookie': str(cookie)})
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(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 cookie = Cookie.SmartCookie()
558 cookie['roundup_user'] = user
559 cookie['roundup_user']['path'] = path
560 self.header({'Set-Cookie': str(cookie)})
561 return self.index()
563 def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
564 nre=re.compile(r'new(\w+)')):
566 # determine the uid to use
567 self.db = self.instance.open('admin')
568 cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
569 user = 'anonymous'
570 if (cookie.has_key('roundup_user') and
571 cookie['roundup_user'].value != 'deleted'):
572 cookie = cookie['roundup_user'].value
573 user, password = base64.decodestring(cookie).split(':')
574 # make sure the user exists
575 try:
576 uid = self.db.user.lookup(user)
577 # now validate the password
578 if password != self.db.user.get(uid, 'password'):
579 user = 'anonymous'
580 except KeyError:
581 user = 'anonymous'
583 # make sure the anonymous user is valid if we're using it
584 if user == 'anonymous':
585 self.make_user_anonymous()
586 else:
587 self.user = user
588 self.db.close()
590 # re-open the database for real, using the user
591 self.db = self.instance.open(self.user)
593 # now figure which function to call
594 path = self.split_path
595 if not path or path[0] in ('', 'index'):
596 self.index()
597 elif len(path) == 1:
598 if path[0] == 'list_classes':
599 self.classes()
600 return
601 if path[0] == 'login':
602 self.login()
603 return
604 if path[0] == 'login_action':
605 self.login_action()
606 return
607 if path[0] == 'newuser_action':
608 self.newuser_action()
609 return
610 if path[0] == 'logout':
611 self.logout()
612 return
613 m = dre.match(path[0])
614 if m:
615 self.classname = m.group(1)
616 self.nodeid = m.group(2)
617 try:
618 cl = self.db.classes[self.classname]
619 except KeyError:
620 raise NotFound
621 try:
622 cl.get(self.nodeid, 'id')
623 except IndexError:
624 raise NotFound
625 try:
626 getattr(self, 'show%s'%self.classname)()
627 except AttributeError:
628 raise NotFound
629 return
630 m = nre.match(path[0])
631 if m:
632 self.classname = m.group(1)
633 try:
634 getattr(self, 'new%s'%self.classname)()
635 except AttributeError:
636 raise NotFound
637 return
638 self.classname = path[0]
639 try:
640 self.db.getclass(self.classname)
641 except KeyError:
642 raise NotFound
643 self.list()
644 else:
645 raise 'ValueError', 'Path not understood'
647 def __del__(self):
648 self.db.close()
651 class ExtendedClient(Client):
652 '''Includes pages and page heading information that relate to the
653 extended schema.
654 '''
655 showsupport = Client.shownode
656 showtimelog = Client.shownode
657 newsupport = Client.newnode
658 newtimelog = Client.newnode
660 default_index_sort = ['-activity']
661 default_index_group = ['priority']
662 default_index_filter = []
663 default_index_columns = ['activity','status','title','assignedto']
664 default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
666 def pagehead(self, title, message=None):
667 url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
668 machine = self.env['SERVER_NAME']
669 port = self.env['SERVER_PORT']
670 if port != '80': machine = machine + ':' + port
671 base = urlparse.urlunparse(('http', machine, url, None, None, None))
672 if message is not None:
673 message = '<div class="system-msg">%s</div>'%message
674 else:
675 message = ''
676 style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
677 user_name = self.user or ''
678 if self.user == 'admin':
679 admin_links = ' | <a href="list_classes">Class List</a>'
680 else:
681 admin_links = ''
682 if self.user not in (None, 'anonymous'):
683 userid = self.db.user.lookup(self.user)
684 user_info = '''
685 <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> |
686 <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> |
687 <a href="user%s">My Details</a> | <a href="logout">Logout</a>
688 '''%(userid, userid, userid)
689 else:
690 user_info = '<a href="login">Login</a>'
691 if self.user is not None:
692 add_links = '''
693 | Add
694 <a href="newissue">Issue</a>,
695 <a href="newsupport">Support</a>,
696 <a href="newuser">User</a>
697 '''
698 else:
699 add_links = ''
700 self.write('''<html><head>
701 <title>%s</title>
702 <style type="text/css">%s</style>
703 </head>
704 <body bgcolor=#ffffff>
705 %s
706 <table width=100%% border=0 cellspacing=0 cellpadding=2>
707 <tr class="location-bar"><td><big><strong>%s</strong></big></td>
708 <td align=right valign=bottom>%s</td></tr>
709 <tr class="location-bar">
710 <td align=left>All
711 <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>,
712 <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>
713 | Unassigned
714 <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>,
715 <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>
716 %s
717 %s</td>
718 <td align=right>%s</td>
719 </table>
720 '''%(title, style, message, title, user_name, add_links, admin_links,
721 user_info))
723 def parsePropsFromForm(cl, form, nodeid=0):
724 '''Pull properties for the given class out of the form.
725 '''
726 props = {}
727 changed = []
728 keys = form.keys()
729 num_re = re.compile('^\d+$')
730 for key in keys:
731 if not cl.properties.has_key(key):
732 continue
733 proptype = cl.properties[key]
734 if isinstance(proptype, hyperdb.String):
735 value = form[key].value.strip()
736 elif isinstance(proptype, hyperdb.Password):
737 value = password.Password(form[key].value.strip())
738 elif isinstance(proptype, hyperdb.Date):
739 value = date.Date(form[key].value.strip())
740 elif isinstance(proptype, hyperdb.Interval):
741 value = date.Interval(form[key].value.strip())
742 elif isinstance(proptype, hyperdb.Link):
743 value = form[key].value.strip()
744 # handle key values
745 link = cl.properties[key].classname
746 if not num_re.match(value):
747 try:
748 value = self.db.classes[link].lookup(value)
749 except:
750 raise ValueError, 'property "%s": %s not a %s'%(
751 key, value, link)
752 elif isinstance(proptype, hyperdb.Multilink):
753 value = form[key]
754 if type(value) != type([]):
755 value = [i.strip() for i in value.value.split(',')]
756 else:
757 value = [i.value.strip() for i in value]
758 link = cl.properties[key].classname
759 l = []
760 for entry in map(str, value):
761 if not num_re.match(entry):
762 try:
763 entry = self.db.classes[link].lookup(entry)
764 except:
765 raise ValueError, \
766 'property "%s": %s not a %s'%(key,
767 entry, link)
768 l.append(entry)
769 l.sort()
770 value = l
771 props[key] = value
772 # if changed, set it
773 if nodeid and value != cl.get(nodeid, key):
774 changed.append(key)
775 props[key] = value
776 return props, changed
778 #
779 # $Log: not supported by cvs2svn $
780 # Revision 1.29 2001/10/09 07:25:59 richard
781 # Added the Password property type. See "pydoc roundup.password" for
782 # implementation details. Have updated some of the documentation too.
783 #
784 # Revision 1.28 2001/10/08 00:34:31 richard
785 # Change message was stuffing up for multilinks with no key property.
786 #
787 # Revision 1.27 2001/10/05 02:23:24 richard
788 # . roundup-admin create now prompts for property info if none is supplied
789 # on the command-line.
790 # . hyperdb Class getprops() method may now return only the mutable
791 # properties.
792 # . Login now uses cookies, which makes it a whole lot more flexible. We can
793 # now support anonymous user access (read-only, unless there's an
794 # "anonymous" user, in which case write access is permitted). Login
795 # handling has been moved into cgi_client.Client.main()
796 # . The "extended" schema is now the default in roundup init.
797 # . The schemas have had their page headings modified to cope with the new
798 # login handling. Existing installations should copy the interfaces.py
799 # file from the roundup lib directory to their instance home.
800 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
801 # Ping - has been removed.
802 # . Fixed a whole bunch of places in the CGI interface where we should have
803 # been returning Not Found instead of throwing an exception.
804 # . Fixed a deviation from the spec: trying to modify the 'id' property of
805 # an item now throws an exception.
806 #
807 # Revision 1.26 2001/09/12 08:31:42 richard
808 # handle cases where mime type is not guessable
809 #
810 # Revision 1.25 2001/08/29 05:30:49 richard
811 # change messages weren't being saved when there was no-one on the nosy list.
812 #
813 # Revision 1.24 2001/08/29 04:49:39 richard
814 # didn't clean up fully after debugging :(
815 #
816 # Revision 1.23 2001/08/29 04:47:18 richard
817 # Fixed CGI client change messages so they actually include the properties
818 # changed (again).
819 #
820 # Revision 1.22 2001/08/17 00:08:10 richard
821 # reverted back to sending messages always regardless of who is doing the web
822 # edit. change notes weren't being saved. bleah. hackish.
823 #
824 # Revision 1.21 2001/08/15 23:43:18 richard
825 # Fixed some isFooTypes that I missed.
826 # Refactored some code in the CGI code.
827 #
828 # Revision 1.20 2001/08/12 06:32:36 richard
829 # using isinstance(blah, Foo) now instead of isFooType
830 #
831 # Revision 1.19 2001/08/07 00:24:42 richard
832 # stupid typo
833 #
834 # Revision 1.18 2001/08/07 00:15:51 richard
835 # Added the copyright/license notice to (nearly) all files at request of
836 # Bizar Software.
837 #
838 # Revision 1.17 2001/08/02 06:38:17 richard
839 # Roundupdb now appends "mailing list" information to its messages which
840 # include the e-mail address and web interface address. Templates may
841 # override this in their db classes to include specific information (support
842 # instructions, etc).
843 #
844 # Revision 1.16 2001/08/02 05:55:25 richard
845 # Web edit messages aren't sent to the person who did the edit any more. No
846 # message is generated if they are the only person on the nosy list.
847 #
848 # Revision 1.15 2001/08/02 00:34:10 richard
849 # bleah syntax error
850 #
851 # Revision 1.14 2001/08/02 00:26:16 richard
852 # Changed the order of the information in the message generated by web edits.
853 #
854 # Revision 1.13 2001/07/30 08:12:17 richard
855 # Added time logging and file uploading to the templates.
856 #
857 # Revision 1.12 2001/07/30 06:26:31 richard
858 # Added some documentation on how the newblah works.
859 #
860 # Revision 1.11 2001/07/30 06:17:45 richard
861 # Features:
862 # . Added ability for cgi newblah forms to indicate that the new node
863 # should be linked somewhere.
864 # Fixed:
865 # . Fixed the agument handling for the roundup-admin find command.
866 # . Fixed handling of summary when no note supplied for newblah. Again.
867 # . Fixed detection of no form in htmltemplate Field display.
868 #
869 # Revision 1.10 2001/07/30 02:37:34 richard
870 # Temporary measure until we have decent schema migration...
871 #
872 # Revision 1.9 2001/07/30 01:25:07 richard
873 # Default implementation is now "classic" rather than "extended" as one would
874 # expect.
875 #
876 # Revision 1.8 2001/07/29 08:27:40 richard
877 # Fixed handling of passed-in values in form elements (ie. during a
878 # drill-down)
879 #
880 # Revision 1.7 2001/07/29 07:01:39 richard
881 # Added vim command to all source so that we don't get no steenkin' tabs :)
882 #
883 # Revision 1.6 2001/07/29 04:04:00 richard
884 # Moved some code around allowing for subclassing to change behaviour.
885 #
886 # Revision 1.5 2001/07/28 08:16:52 richard
887 # New issue form handles lack of note better now.
888 #
889 # Revision 1.4 2001/07/28 00:34:34 richard
890 # Fixed some non-string node ids.
891 #
892 # Revision 1.3 2001/07/23 03:56:30 richard
893 # oops, missed a config removal
894 #
895 # Revision 1.2 2001/07/22 12:09:32 richard
896 # Final commit of Grande Splite
897 #
898 # Revision 1.1 2001/07/22 11:58:35 richard
899 # More Grande Splite
900 #
901 #
902 # vim: set filetype=python ts=4 sw=4 et si