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.37 2001-10-21 07:26:35 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 not path:
622 raise 'ValueError', 'Path not understood'
624 #
625 # Everthing ignores path[1:]
626 #
627 # The file download link generator actually relies on this - it
628 # appends the name of the file to the URL so the download file name
629 # is correct, but doesn't actually use it.
630 action = path[0]
631 if action == 'list_classes':
632 self.classes()
633 return
634 if action == 'login':
635 self.login()
636 return
637 if action == 'login_action':
638 self.login_action()
639 return
640 if action == 'newuser_action':
641 self.newuser_action()
642 return
643 if action == 'logout':
644 self.logout()
645 return
646 m = dre.match(action)
647 if m:
648 self.classname = m.group(1)
649 self.nodeid = m.group(2)
650 try:
651 cl = self.db.classes[self.classname]
652 except KeyError:
653 raise NotFound
654 try:
655 cl.get(self.nodeid, 'id')
656 except IndexError:
657 raise NotFound
658 try:
659 func = getattr(self, 'show%s'%self.classname)
660 except AttributeError:
661 raise NotFound
662 func()
663 return
664 m = nre.match(action)
665 if m:
666 self.classname = m.group(1)
667 try:
668 func = getattr(self, 'new%s'%self.classname)
669 except AttributeError:
670 raise NotFound
671 func()
672 return
673 self.classname = action
674 try:
675 self.db.getclass(self.classname)
676 except KeyError:
677 raise NotFound
678 self.list()
680 def __del__(self):
681 self.db.close()
684 class ExtendedClient(Client):
685 '''Includes pages and page heading information that relate to the
686 extended schema.
687 '''
688 showsupport = Client.shownode
689 showtimelog = Client.shownode
690 newsupport = Client.newnode
691 newtimelog = Client.newnode
693 default_index_sort = ['-activity']
694 default_index_group = ['priority']
695 default_index_filter = ['status']
696 default_index_columns = ['activity','status','title','assignedto']
697 default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
699 def pagehead(self, title, message=None):
700 url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
701 machine = self.env['SERVER_NAME']
702 port = self.env['SERVER_PORT']
703 if port != '80': machine = machine + ':' + port
704 base = urlparse.urlunparse(('http', machine, url, None, None, None))
705 if message is not None:
706 message = '<div class="system-msg">%s</div>'%message
707 else:
708 message = ''
709 style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
710 user_name = self.user or ''
711 if self.user == 'admin':
712 admin_links = ' | <a href="list_classes">Class List</a>'
713 else:
714 admin_links = ''
715 if self.user not in (None, 'anonymous'):
716 userid = self.db.user.lookup(self.user)
717 user_info = '''
718 <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> |
719 <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> |
720 <a href="user%s">My Details</a> | <a href="logout">Logout</a>
721 '''%(userid, userid, userid)
722 else:
723 user_info = '<a href="login">Login</a>'
724 if self.user is not None:
725 add_links = '''
726 | Add
727 <a href="newissue">Issue</a>,
728 <a href="newsupport">Support</a>,
729 <a href="newuser">User</a>
730 '''
731 else:
732 add_links = ''
733 self.write('''<html><head>
734 <title>%s</title>
735 <style type="text/css">%s</style>
736 </head>
737 <body bgcolor=#ffffff>
738 %s
739 <table width=100%% border=0 cellspacing=0 cellpadding=2>
740 <tr class="location-bar"><td><big><strong>%s</strong></big></td>
741 <td align=right valign=bottom>%s</td></tr>
742 <tr class="location-bar">
743 <td align=left>All
744 <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>,
745 <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>
746 | Unassigned
747 <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>,
748 <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>
749 %s
750 %s</td>
751 <td align=right>%s</td>
752 </table>
753 '''%(title, style, message, title, user_name, add_links, admin_links,
754 user_info))
756 def parsePropsFromForm(db, cl, form, nodeid=0):
757 '''Pull properties for the given class out of the form.
758 '''
759 props = {}
760 changed = []
761 keys = form.keys()
762 num_re = re.compile('^\d+$')
763 for key in keys:
764 if not cl.properties.has_key(key):
765 continue
766 proptype = cl.properties[key]
767 if isinstance(proptype, hyperdb.String):
768 value = form[key].value.strip()
769 elif isinstance(proptype, hyperdb.Password):
770 value = password.Password(form[key].value.strip())
771 elif isinstance(proptype, hyperdb.Date):
772 value = date.Date(form[key].value.strip())
773 elif isinstance(proptype, hyperdb.Interval):
774 value = date.Interval(form[key].value.strip())
775 elif isinstance(proptype, hyperdb.Link):
776 value = form[key].value.strip()
777 # see if it's the "no selection" choice
778 if value == '-1':
779 # don't set this property
780 continue
781 else:
782 # handle key values
783 link = cl.properties[key].classname
784 if not num_re.match(value):
785 try:
786 value = db.classes[link].lookup(value)
787 except KeyError:
788 raise ValueError, 'property "%s": %s not a %s'%(
789 key, value, link)
790 elif isinstance(proptype, hyperdb.Multilink):
791 value = form[key]
792 if type(value) != type([]):
793 value = [i.strip() for i in value.value.split(',')]
794 else:
795 value = [i.value.strip() for i in value]
796 link = cl.properties[key].classname
797 l = []
798 for entry in map(str, value):
799 if not num_re.match(entry):
800 try:
801 entry = db.classes[link].lookup(entry)
802 except KeyError:
803 raise ValueError, \
804 'property "%s": "%s" not an entry of %s'%(key,
805 entry, link.capitalize())
806 l.append(entry)
807 l.sort()
808 value = l
809 props[key] = value
810 # if changed, set it
811 if nodeid and value != cl.get(nodeid, key):
812 changed.append(key)
813 props[key] = value
814 return props, changed
816 #
817 # $Log: not supported by cvs2svn $
818 # Revision 1.36 2001/10/21 04:44:50 richard
819 # bug #473124: UI inconsistency with Link fields.
820 # This also prompted me to fix a fairly long-standing usability issue -
821 # that of being able to turn off certain filters.
822 #
823 # Revision 1.35 2001/10/21 00:17:54 richard
824 # CGI interface view customisation section may now be hidden (patch from
825 # Roch'e Compaan.)
826 #
827 # Revision 1.34 2001/10/20 11:58:48 richard
828 # Catch errors in login - no username or password supplied.
829 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
830 #
831 # Revision 1.33 2001/10/17 00:18:41 richard
832 # Manually constructing cookie headers now.
833 #
834 # Revision 1.32 2001/10/16 03:36:21 richard
835 # CGI interface wasn't handling checkboxes at all.
836 #
837 # Revision 1.31 2001/10/14 10:55:00 richard
838 # Handle empty strings in HTML template Link function
839 #
840 # Revision 1.30 2001/10/09 07:38:58 richard
841 # Pushed the base code for the extended schema CGI interface back into the
842 # code cgi_client module so that future updates will be less painful.
843 # Also removed a debugging print statement from cgi_client.
844 #
845 # Revision 1.29 2001/10/09 07:25:59 richard
846 # Added the Password property type. See "pydoc roundup.password" for
847 # implementation details. Have updated some of the documentation too.
848 #
849 # Revision 1.28 2001/10/08 00:34:31 richard
850 # Change message was stuffing up for multilinks with no key property.
851 #
852 # Revision 1.27 2001/10/05 02:23:24 richard
853 # . roundup-admin create now prompts for property info if none is supplied
854 # on the command-line.
855 # . hyperdb Class getprops() method may now return only the mutable
856 # properties.
857 # . Login now uses cookies, which makes it a whole lot more flexible. We can
858 # now support anonymous user access (read-only, unless there's an
859 # "anonymous" user, in which case write access is permitted). Login
860 # handling has been moved into cgi_client.Client.main()
861 # . The "extended" schema is now the default in roundup init.
862 # . The schemas have had their page headings modified to cope with the new
863 # login handling. Existing installations should copy the interfaces.py
864 # file from the roundup lib directory to their instance home.
865 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
866 # Ping - has been removed.
867 # . Fixed a whole bunch of places in the CGI interface where we should have
868 # been returning Not Found instead of throwing an exception.
869 # . Fixed a deviation from the spec: trying to modify the 'id' property of
870 # an item now throws an exception.
871 #
872 # Revision 1.26 2001/09/12 08:31:42 richard
873 # handle cases where mime type is not guessable
874 #
875 # Revision 1.25 2001/08/29 05:30:49 richard
876 # change messages weren't being saved when there was no-one on the nosy list.
877 #
878 # Revision 1.24 2001/08/29 04:49:39 richard
879 # didn't clean up fully after debugging :(
880 #
881 # Revision 1.23 2001/08/29 04:47:18 richard
882 # Fixed CGI client change messages so they actually include the properties
883 # changed (again).
884 #
885 # Revision 1.22 2001/08/17 00:08:10 richard
886 # reverted back to sending messages always regardless of who is doing the web
887 # edit. change notes weren't being saved. bleah. hackish.
888 #
889 # Revision 1.21 2001/08/15 23:43:18 richard
890 # Fixed some isFooTypes that I missed.
891 # Refactored some code in the CGI code.
892 #
893 # Revision 1.20 2001/08/12 06:32:36 richard
894 # using isinstance(blah, Foo) now instead of isFooType
895 #
896 # Revision 1.19 2001/08/07 00:24:42 richard
897 # stupid typo
898 #
899 # Revision 1.18 2001/08/07 00:15:51 richard
900 # Added the copyright/license notice to (nearly) all files at request of
901 # Bizar Software.
902 #
903 # Revision 1.17 2001/08/02 06:38:17 richard
904 # Roundupdb now appends "mailing list" information to its messages which
905 # include the e-mail address and web interface address. Templates may
906 # override this in their db classes to include specific information (support
907 # instructions, etc).
908 #
909 # Revision 1.16 2001/08/02 05:55:25 richard
910 # Web edit messages aren't sent to the person who did the edit any more. No
911 # message is generated if they are the only person on the nosy list.
912 #
913 # Revision 1.15 2001/08/02 00:34:10 richard
914 # bleah syntax error
915 #
916 # Revision 1.14 2001/08/02 00:26:16 richard
917 # Changed the order of the information in the message generated by web edits.
918 #
919 # Revision 1.13 2001/07/30 08:12:17 richard
920 # Added time logging and file uploading to the templates.
921 #
922 # Revision 1.12 2001/07/30 06:26:31 richard
923 # Added some documentation on how the newblah works.
924 #
925 # Revision 1.11 2001/07/30 06:17:45 richard
926 # Features:
927 # . Added ability for cgi newblah forms to indicate that the new node
928 # should be linked somewhere.
929 # Fixed:
930 # . Fixed the agument handling for the roundup-admin find command.
931 # . Fixed handling of summary when no note supplied for newblah. Again.
932 # . Fixed detection of no form in htmltemplate Field display.
933 #
934 # Revision 1.10 2001/07/30 02:37:34 richard
935 # Temporary measure until we have decent schema migration...
936 #
937 # Revision 1.9 2001/07/30 01:25:07 richard
938 # Default implementation is now "classic" rather than "extended" as one would
939 # expect.
940 #
941 # Revision 1.8 2001/07/29 08:27:40 richard
942 # Fixed handling of passed-in values in form elements (ie. during a
943 # drill-down)
944 #
945 # Revision 1.7 2001/07/29 07:01:39 richard
946 # Added vim command to all source so that we don't get no steenkin' tabs :)
947 #
948 # Revision 1.6 2001/07/29 04:04:00 richard
949 # Moved some code around allowing for subclassing to change behaviour.
950 #
951 # Revision 1.5 2001/07/28 08:16:52 richard
952 # New issue form handles lack of note better now.
953 #
954 # Revision 1.4 2001/07/28 00:34:34 richard
955 # Fixed some non-string node ids.
956 #
957 # Revision 1.3 2001/07/23 03:56:30 richard
958 # oops, missed a config removal
959 #
960 # Revision 1.2 2001/07/22 12:09:32 richard
961 # Final commit of Grande Splite
962 #
963 # Revision 1.1 2001/07/22 11:58:35 richard
964 # More Grande Splite
965 #
966 #
967 # vim: set filetype=python ts=4 sw=4 et si