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