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