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.86 2001-12-20 15:43:01 rochecompaan Exp $
20 __doc__ = """
21 WWW request handler (also used in the stand-alone server).
22 """
24 import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
25 import binascii, Cookie, time
27 import roundupdb, htmltemplate, date, hyperdb, password
28 from roundup.i18n import _
30 class Unauthorised(ValueError):
31 pass
33 class NotFound(ValueError):
34 pass
36 class Client:
37 '''
38 A note about login
39 ------------------
41 If the user has no login cookie, then they are anonymous. There
42 are two levels of anonymous use. If there is no 'anonymous' user, there
43 is no login at all and the database is opened in read-only mode. If the
44 'anonymous' user exists, the user is logged in using that user (though
45 there is no cookie). This allows them to modify the database, and all
46 modifications are attributed to the 'anonymous' user.
49 Customisation
50 -------------
51 FILTER_POSITION - one of 'top', 'bottom', 'top and bottom'
52 ANONYMOUS_ACCESS - one of 'deny', 'allow'
53 ANONYMOUS_REGISTER - one of 'deny', 'allow'
55 from the roundup class:
56 INSTANCE_NAME - defaults to 'Roundup issue tracker'
58 '''
59 FILTER_POSITION = 'bottom' # one of 'top', 'bottom', 'top and bottom'
60 ANONYMOUS_ACCESS = 'deny' # one of 'deny', 'allow'
61 ANONYMOUS_REGISTER = 'deny' # one of 'deny', 'allow'
63 def __init__(self, instance, request, env, form=None):
64 self.instance = instance
65 self.request = request
66 self.env = env
67 self.path = env['PATH_INFO']
68 self.split_path = self.path.split('/')
70 if form is None:
71 self.form = cgi.FieldStorage(environ=env)
72 else:
73 self.form = form
74 self.headers_done = 0
75 try:
76 self.debug = int(env.get("ROUNDUP_DEBUG", 0))
77 except ValueError:
78 # someone gave us a non-int debug level, turn it off
79 self.debug = 0
81 def getuid(self):
82 return self.db.user.lookup(self.user)
84 def header(self, headers={'Content-Type':'text/html'}):
85 '''Put up the appropriate header.
86 '''
87 if not headers.has_key('Content-Type'):
88 headers['Content-Type'] = 'text/html'
89 self.request.send_response(200)
90 for entry in headers.items():
91 self.request.send_header(*entry)
92 self.request.end_headers()
93 self.headers_done = 1
94 if self.debug:
95 self.headers_sent = headers
97 def pagehead(self, title, message=None):
98 url = self.env['SCRIPT_NAME'] + '/'
99 machine = self.env['SERVER_NAME']
100 port = self.env['SERVER_PORT']
101 if port != '80': machine = machine + ':' + port
102 base = urlparse.urlunparse(('http', machine, url, None, None, None))
103 if message is not None:
104 message = _('<div class="system-msg">%(message)s</div>')%locals()
105 else:
106 message = ''
107 style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
108 user_name = self.user or ''
109 if self.user == 'admin':
110 admin_links = _(' | <a href="list_classes">Class List</a>' \
111 ' | <a href="user">User List</a>')
112 else:
113 admin_links = ''
114 if self.user not in (None, 'anonymous'):
115 userid = self.db.user.lookup(self.user)
116 user_info = _('''
117 <a href="issue?assignedto=%(userid)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> |
118 <a href="user%(userid)s">My Details</a> | <a href="logout">Logout</a>
119 ''')%locals()
120 else:
121 user_info = _('<a href="login">Login</a>')
122 if self.user is not None:
123 if self.user == 'admin':
124 add_links = _('''
125 | Add
126 <a href="newissue">Issue</a>,
127 <a href="newuser">User</a>
128 ''')
129 else:
130 add_links = _('''
131 | Add
132 <a href="newissue">Issue</a>
133 ''')
134 else:
135 add_links = ''
136 self.write(_('''<html><head>
137 <title>%(title)s</title>
138 <style type="text/css">%(style)s</style>
139 </head>
140 <body bgcolor=#ffffff>
141 %(message)s
142 <table width=100%% border=0 cellspacing=0 cellpadding=2>
143 <tr class="location-bar"><td><big><strong>%(title)s</strong></big></td>
144 <td align=right valign=bottom>%(user_name)s</td></tr>
145 <tr class="location-bar">
146 <td align=left>All
147 <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>
148 | Unassigned
149 <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>
150 %(add_links)s
151 %(admin_links)s</td>
152 <td align=right>%(user_info)s</td>
153 </table>
154 ''')%locals())
156 def pagefoot(self):
157 if self.debug:
158 self.write(_('<hr><small><dl><dt><b>Path</b></dt>'))
159 self.write('<dd>%s</dd>'%(', '.join(map(repr, self.split_path))))
160 keys = self.form.keys()
161 keys.sort()
162 if keys:
163 self.write(_('<dt><b>Form entries</b></dt>'))
164 for k in self.form.keys():
165 v = self.form.getvalue(k, "<empty>")
166 if type(v) is type([]):
167 # Multiple username fields specified
168 v = "|".join(v)
169 self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
170 keys = self.headers_sent.keys()
171 keys.sort()
172 self.write(_('<dt><b>Sent these HTTP headers</b></dt>'))
173 for k in keys:
174 v = self.headers_sent[k]
175 self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
176 keys = self.env.keys()
177 keys.sort()
178 self.write(_('<dt><b>CGI environment</b></dt>'))
179 for k in keys:
180 v = self.env[k]
181 self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
182 self.write('</dl></small>')
183 self.write('</body></html>')
185 def write(self, content):
186 if not self.headers_done:
187 self.header()
188 self.request.wfile.write(content)
190 def index_arg(self, arg):
191 ''' handle the args to index - they might be a list from the form
192 (ie. submitted from a form) or they might be a command-separated
193 single string (ie. manually constructed GET args)
194 '''
195 if self.form.has_key(arg):
196 arg = self.form[arg]
197 if type(arg) == type([]):
198 return [arg.value for arg in arg]
199 return arg.value.split(',')
200 return []
202 def index_filterspec(self, filter):
203 ''' pull the index filter spec from the form
205 Links and multilinks want to be lists - the rest are straight
206 strings.
207 '''
208 props = self.db.classes[self.classname].getprops()
209 # all the form args not starting with ':' are filters
210 filterspec = {}
211 for key in self.form.keys():
212 if key[0] == ':': continue
213 if not props.has_key(key): continue
214 if key not in filter: continue
215 prop = props[key]
216 value = self.form[key]
217 if (isinstance(prop, hyperdb.Link) or
218 isinstance(prop, hyperdb.Multilink)):
219 if type(value) == type([]):
220 value = [arg.value for arg in value]
221 else:
222 value = value.value.split(',')
223 l = filterspec.get(key, [])
224 l = l + value
225 filterspec[key] = l
226 else:
227 filterspec[key] = value.value
228 return filterspec
230 def customization_widget(self):
231 ''' The customization widget is visible by default. The widget
232 visibility is remembered by show_customization. Visibility
233 is not toggled if the action value is "Redisplay"
234 '''
235 if not self.form.has_key('show_customization'):
236 visible = 1
237 else:
238 visible = int(self.form['show_customization'].value)
239 if self.form.has_key('action'):
240 if self.form['action'].value != 'Redisplay':
241 visible = self.form['action'].value == '+'
243 return visible
245 default_index_sort = ['-activity']
246 default_index_group = ['priority']
247 default_index_filter = ['status']
248 default_index_columns = ['id','activity','title','status','assignedto']
249 default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
250 def index(self):
251 ''' put up an index
252 '''
253 self.classname = 'issue'
254 # see if the web has supplied us with any customisation info
255 defaults = 1
256 for key in ':sort', ':group', ':filter', ':columns':
257 if self.form.has_key(key):
258 defaults = 0
259 break
260 if defaults:
261 # no info supplied - use the defaults
262 sort = self.default_index_sort
263 group = self.default_index_group
264 filter = self.default_index_filter
265 columns = self.default_index_columns
266 filterspec = self.default_index_filterspec
267 else:
268 sort = self.index_arg(':sort')
269 group = self.index_arg(':group')
270 filter = self.index_arg(':filter')
271 columns = self.index_arg(':columns')
272 filterspec = self.index_filterspec(filter)
273 return self.list(columns=columns, filter=filter, group=group,
274 sort=sort, filterspec=filterspec)
276 # XXX deviates from spec - loses the '+' (that's a reserved character
277 # in URLS
278 def list(self, sort=None, group=None, filter=None, columns=None,
279 filterspec=None, show_customization=None):
280 ''' call the template index with the args
282 :sort - sort by prop name, optionally preceeded with '-'
283 to give descending or nothing for ascending sorting.
284 :group - group by prop name, optionally preceeded with '-' or
285 to sort in descending or nothing for ascending order.
286 :filter - selects which props should be displayed in the filter
287 section. Default is all.
288 :columns - selects the columns that should be displayed.
289 Default is all.
291 '''
292 cn = self.classname
293 cl = self.db.classes[cn]
294 self.pagehead(_('%(instancename)s: Index of %(classname)s')%{
295 'classname': cn, 'instancename': self.INSTANCE_NAME})
296 if sort is None: sort = self.index_arg(':sort')
297 if group is None: group = self.index_arg(':group')
298 if filter is None: filter = self.index_arg(':filter')
299 if columns is None: columns = self.index_arg(':columns')
300 if filterspec is None: filterspec = self.index_filterspec(filter)
301 if show_customization is None:
302 show_customization = self.customization_widget()
304 index = htmltemplate.IndexTemplate(self, self.TEMPLATES, cn)
305 index.render(filterspec, filter, columns, sort, group,
306 show_customization=show_customization)
307 self.pagefoot()
309 def shownode(self, message=None):
310 ''' display an item
311 '''
312 cn = self.classname
313 cl = self.db.classes[cn]
315 # possibly perform an edit
316 keys = self.form.keys()
317 num_re = re.compile('^\d+$')
318 # don't try to set properties if the user has just logged in
319 if keys and not self.form.has_key('__login_name'):
320 try:
321 props, changed = parsePropsFromForm(self.db, cl, self.form,
322 self.nodeid)
323 # make changes to the node
324 self._changenode(props)
325 # handle linked nodes
326 self._post_editnode(self.nodeid)
327 # and some nice feedback for the user
328 if changed:
329 message = _('%(changes)s edited ok')%{'changes':
330 ', '.join(changed.keys())}
331 elif self.form.has_key('__note') and self.form['__note'].value:
332 message = _('note added')
333 elif self.form.has_key('__file'):
334 message = _('file added')
335 else:
336 message = _('nothing changed')
337 except:
338 self.db.rollback()
339 s = StringIO.StringIO()
340 traceback.print_exc(None, s)
341 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
343 # now the display
344 id = self.nodeid
345 if cl.getkey():
346 id = cl.get(id, cl.getkey())
347 self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
349 nodeid = self.nodeid
351 # use the template to display the item
352 item = htmltemplate.ItemTemplate(self, self.TEMPLATES, self.classname)
353 item.render(nodeid)
355 self.pagefoot()
356 showissue = shownode
357 showmsg = shownode
359 def _changenode(self, props):
360 ''' change the node based on the contents of the form
361 '''
362 cl = self.db.classes[self.classname]
363 # set status to chatting if 'unread' or 'resolved'
364 try:
365 # determine the id of 'unread','resolved' and 'chatting'
366 unread_id = self.db.status.lookup('unread')
367 resolved_id = self.db.status.lookup('resolved')
368 chatting_id = self.db.status.lookup('chatting')
369 current_status = cl.get(self.nodeid, 'status')
370 except KeyError:
371 pass
372 else:
373 if (props['status'] == unread_id or props['status'] == resolved_id and current_status == resolved_id):
374 props['status'] = chatting_id
375 # add assignedto to the nosy list
376 if props.has_key('assignedto'):
377 assignedto_id = props['assignedto']
378 if assignedto_id not in props['nosy']:
379 props['nosy'].append(assignedto_id)
380 # create the message
381 message, files = self._handle_message()
382 if message:
383 props['messages'] = cl.get(self.nodeid, 'messages') + [message]
384 if files:
385 props['files'] = cl.get(self.nodeid, 'files') + files
386 # make the changes
387 cl.set(self.nodeid, **props)
389 def _createnode(self):
390 ''' create a node based on the contents of the form
391 '''
392 cl = self.db.classes[self.classname]
393 props, dummy = parsePropsFromForm(self.db, cl, self.form)
395 # set status to 'unread' if not specified - a status of '- no
396 # selection -' doesn't make sense
397 if not props.has_key('status'):
398 try:
399 unread_id = self.db.status.lookup('unread')
400 except KeyError:
401 pass
402 else:
403 props['status'] = unread_id
404 # add assignedto to the nosy list
405 if props.has_key('assignedto'):
406 assignedto_id = props['assignedto']
407 if props.has_key('nosy') and assignedto_id not in props['nosy']:
408 props['nosy'].append(assignedto_id)
409 else:
410 props['nosy'] = [assignedto_id]
411 # check for messages and files
412 message, files = self._handle_message()
413 if message:
414 props['messages'] = [message]
415 if files:
416 props['files'] = files
417 # create the node and return it's id
418 return cl.create(**props)
420 def _handle_message(self):
421 ''' generate and edit message
422 '''
423 # handle file attachments
424 files = []
425 if self.form.has_key('__file'):
426 file = self.form['__file']
427 if file.filename:
428 filename = file.filename.split('\\')[-1]
429 mime_type = mimetypes.guess_type(filename)[0]
430 if not mime_type:
431 mime_type = "application/octet-stream"
432 # create the new file entry
433 files.append(self.db.file.create(type=mime_type,
434 name=filename, content=file.file.read()))
436 # we don't want to do a message if none of the following is true...
437 cn = self.classname
438 cl = self.db.classes[self.classname]
439 props = cl.getprops()
440 note = None
441 # in a nutshell, don't do anything if there's no note or there's no
442 # nosy
443 if self.form.has_key('__note'):
444 note = self.form['__note'].value
445 if not props.has_key('messages'):
446 return None, files
447 if not isinstance(props['messages'], hyperdb.Multilink):
448 return None, files
449 if not props['messages'].classname == 'msg':
450 return None, files
451 if not (self.form.has_key('nosy') or note):
452 return None, files
454 # handle the note
455 if note:
456 if '\n' in note:
457 summary = re.split(r'\n\r?', note)[0]
458 else:
459 summary = note
460 m = ['%s\n'%note]
461 elif not files:
462 # don't generate a useless message
463 return None, files
465 # now create the message, attaching the files
466 content = '\n'.join(m)
467 message_id = self.db.msg.create(author=self.getuid(),
468 recipients=[], date=date.Date('.'), summary=summary,
469 content=content, files=files)
471 # update the messages property
472 return message_id, files
474 def _post_editnode(self, nid):
475 '''Do the linking part of the node creation.
477 If a form element has :link or :multilink appended to it, its
478 value specifies a node designator and the property on that node
479 to add _this_ node to as a link or multilink.
481 This is typically used on, eg. the file upload page to indicated
482 which issue to link the file to.
484 TODO: I suspect that this and newfile will go away now that
485 there's the ability to upload a file using the issue __file form
486 element!
487 '''
488 cn = self.classname
489 cl = self.db.classes[cn]
490 # link if necessary
491 keys = self.form.keys()
492 for key in keys:
493 if key == ':multilink':
494 value = self.form[key].value
495 if type(value) != type([]): value = [value]
496 for value in value:
497 designator, property = value.split(':')
498 link, nodeid = roundupdb.splitDesignator(designator)
499 link = self.db.classes[link]
500 value = link.get(nodeid, property)
501 value.append(nid)
502 link.set(nodeid, **{property: value})
503 elif key == ':link':
504 value = self.form[key].value
505 if type(value) != type([]): value = [value]
506 for value in value:
507 designator, property = value.split(':')
508 link, nodeid = roundupdb.splitDesignator(designator)
509 link = self.db.classes[link]
510 link.set(nodeid, **{property: nid})
512 def newnode(self, message=None):
513 ''' Add a new node to the database.
515 The form works in two modes: blank form and submission (that is,
516 the submission goes to the same URL). **Eventually this means that
517 the form will have previously entered information in it if
518 submission fails.
520 The new node will be created with the properties specified in the
521 form submission. For multilinks, multiple form entries are handled,
522 as are prop=value,value,value. You can't mix them though.
524 If the new node is to be referenced from somewhere else immediately
525 (ie. the new node is a file that is to be attached to a support
526 issue) then supply one of these arguments in addition to the usual
527 form entries:
528 :link=designator:property
529 :multilink=designator:property
530 ... which means that once the new node is created, the "property"
531 on the node given by "designator" should now reference the new
532 node's id. The node id will be appended to the multilink.
533 '''
534 cn = self.classname
535 cl = self.db.classes[cn]
537 # possibly perform a create
538 keys = self.form.keys()
539 if [i for i in keys if i[0] != ':']:
540 props = {}
541 try:
542 nid = self._createnode()
543 # handle linked nodes
544 self._post_editnode(nid)
545 # and some nice feedback for the user
546 message = _('%(classname)s created ok')%{'classname': cn}
548 # render the newly created issue
549 self.db.commit()
550 self.nodeid = nid
551 self.pagehead('%s: %s'%(self.classname.capitalize(), nid),
552 message)
553 item = htmltemplate.ItemTemplate(self, self.TEMPLATES,
554 self.classname)
555 item.render(nid)
556 self.pagefoot()
557 return
558 except:
559 self.db.rollback()
560 s = StringIO.StringIO()
561 traceback.print_exc(None, s)
562 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
563 self.pagehead(_('New %(classname)s')%{'classname':
564 self.classname.capitalize()}, message)
566 # call the template
567 newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
568 self.classname)
569 newitem.render(self.form)
571 self.pagefoot()
572 newissue = newnode
574 def newuser(self, message=None):
575 ''' Add a new user to the database.
577 Don't do any of the message or file handling, just create the node.
578 '''
579 cn = self.classname
580 cl = self.db.classes[cn]
582 # possibly perform a create
583 keys = self.form.keys()
584 if [i for i in keys if i[0] != ':']:
585 try:
586 props, dummy = parsePropsFromForm(self.db, cl, self.form)
587 nid = cl.create(**props)
588 # handle linked nodes
589 self._post_editnode(nid)
590 # and some nice feedback for the user
591 message = _('%(classname)s created ok')%{'classname': cn}
592 except:
593 self.db.rollback()
594 s = StringIO.StringIO()
595 traceback.print_exc(None, s)
596 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
597 self.pagehead(_('New %(classname)s')%{'classname':
598 self.classname.capitalize()}, message)
600 # call the template
601 newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
602 self.classname)
603 newitem.render(self.form)
605 self.pagefoot()
607 def newfile(self, message=None):
608 ''' Add a new file to the database.
610 This form works very much the same way as newnode - it just has a
611 file upload.
612 '''
613 cn = self.classname
614 cl = self.db.classes[cn]
616 # possibly perform a create
617 keys = self.form.keys()
618 if [i for i in keys if i[0] != ':']:
619 try:
620 file = self.form['content']
621 mime_type = mimetypes.guess_type(file.filename)[0]
622 if not mime_type:
623 mime_type = "application/octet-stream"
624 # save the file
625 nid = cl.create(content=file.file.read(), type=mime_type,
626 name=file.filename)
627 # handle linked nodes
628 self._post_editnode(nid)
629 # and some nice feedback for the user
630 message = _('%(classname)s created ok')%{'classname': cn}
631 except:
632 self.db.rollback()
633 s = StringIO.StringIO()
634 traceback.print_exc(None, s)
635 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
637 self.pagehead(_('New %(classname)s')%{'classname':
638 self.classname.capitalize()}, message)
639 newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
640 self.classname)
641 newitem.render(self.form)
642 self.pagefoot()
644 def showuser(self, message=None):
645 '''Display a user page for editing. Make sure the user is allowed
646 to edit this node, and also check for password changes.
647 '''
648 if self.user == 'anonymous':
649 raise Unauthorised
651 user = self.db.user
653 # get the username of the node being edited
654 node_user = user.get(self.nodeid, 'username')
656 if self.user not in ('admin', node_user):
657 raise Unauthorised
659 #
660 # perform any editing
661 #
662 keys = self.form.keys()
663 num_re = re.compile('^\d+$')
664 if keys:
665 try:
666 props, changed = parsePropsFromForm(self.db, user, self.form,
667 self.nodeid)
668 set_cookie = 0
669 if self.nodeid == self.getuid() and changed.has_key('password'):
670 password = self.form['password'].value.strip()
671 if password:
672 set_cookie = password
673 else:
674 # no password was supplied - don't change it
675 del props['password']
676 del changed['password']
677 user.set(self.nodeid, **props)
678 # and some feedback for the user
679 message = _('%(changes)s edited ok')%{'changes':
680 ', '.join(changed.keys())}
681 except:
682 self.db.rollback()
683 s = StringIO.StringIO()
684 traceback.print_exc(None, s)
685 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
686 else:
687 set_cookie = 0
689 # fix the cookie if the password has changed
690 if set_cookie:
691 self.set_cookie(self.user, set_cookie)
693 #
694 # now the display
695 #
696 self.pagehead(_('User: %(user)s')%{'user': node_user}, message)
698 # use the template to display the item
699 item = htmltemplate.ItemTemplate(self, self.TEMPLATES, 'user')
700 item.render(self.nodeid)
701 self.pagefoot()
703 def showfile(self):
704 ''' display a file
705 '''
706 nodeid = self.nodeid
707 cl = self.db.file
708 mime_type = cl.get(nodeid, 'type')
709 if mime_type == 'message/rfc822':
710 mime_type = 'text/plain'
711 self.header(headers={'Content-Type': mime_type})
712 self.write(cl.get(nodeid, 'content'))
714 def classes(self, message=None):
715 ''' display a list of all the classes in the database
716 '''
717 if self.user == 'admin':
718 self.pagehead(_('Table of classes'), message)
719 classnames = self.db.classes.keys()
720 classnames.sort()
721 self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
722 for cn in classnames:
723 cl = self.db.getclass(cn)
724 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
725 for key, value in cl.properties.items():
726 if value is None: value = ''
727 else: value = str(value)
728 self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
729 key, cgi.escape(value)))
730 self.write('</table>')
731 self.pagefoot()
732 else:
733 raise Unauthorised
735 def login(self, message=None, newuser_form=None, action='index'):
736 '''Display a login page.
737 '''
738 self.pagehead(_('Login to roundup'), message)
739 self.write(_('''
740 <table>
741 <tr><td colspan=2 class="strong-header">Existing User Login</td></tr>
742 <form action="login_action" method=POST>
743 <input type="hidden" name="__destination_url" value="%(action)s">
744 <tr><td align=right>Login name: </td>
745 <td><input name="__login_name"></td></tr>
746 <tr><td align=right>Password: </td>
747 <td><input type="password" name="__login_password"></td></tr>
748 <tr><td></td>
749 <td><input type="submit" value="Log In"></td></tr>
750 </form>
751 ''')%locals())
752 if self.user is None and self.ANONYMOUS_REGISTER == 'deny':
753 self.write('</table>')
754 self.pagefoot()
755 return
756 values = {'realname': '', 'organisation': '', 'address': '',
757 'phone': '', 'username': '', 'password': '', 'confirm': '',
758 'action': action}
759 if newuser_form is not None:
760 for key in newuser_form.keys():
761 values[key] = newuser_form[key].value
762 self.write(_('''
763 <p>
764 <tr><td colspan=2 class="strong-header">New User Registration</td></tr>
765 <tr><td colspan=2><em>marked items</em> are optional...</td></tr>
766 <form action="newuser_action" method=POST>
767 <input type="hidden" name="__destination_url" value="%(action)s">
768 <tr><td align=right><em>Name: </em></td>
769 <td><input name="realname" value="%(realname)s"></td></tr>
770 <tr><td align=right><em>Organisation: </em></td>
771 <td><input name="organisation" value="%(organisation)s"></td></tr>
772 <tr><td align=right>E-Mail Address: </td>
773 <td><input name="address" value="%(address)s"></td></tr>
774 <tr><td align=right><em>Phone: </em></td>
775 <td><input name="phone" value="%(phone)s"></td></tr>
776 <tr><td align=right>Preferred Login name: </td>
777 <td><input name="username" value="%(username)s"></td></tr>
778 <tr><td align=right>Password: </td>
779 <td><input type="password" name="password" value="%(password)s"></td></tr>
780 <tr><td align=right>Password Again: </td>
781 <td><input type="password" name="confirm" value="%(confirm)s"></td></tr>
782 <tr><td></td>
783 <td><input type="submit" value="Register"></td></tr>
784 </form>
785 </table>
786 ''')%values)
787 self.pagefoot()
789 def login_action(self, message=None):
790 '''Attempt to log a user in and set the cookie
792 returns 0 if a page is generated as a result of this call, and
793 1 if not (ie. the login is successful
794 '''
795 if not self.form.has_key('__login_name'):
796 self.login(message=_('Username required'))
797 return 0
798 self.user = self.form['__login_name'].value
799 if self.form.has_key('__login_password'):
800 password = self.form['__login_password'].value
801 else:
802 password = ''
803 # make sure the user exists
804 try:
805 uid = self.db.user.lookup(self.user)
806 except KeyError:
807 name = self.user
808 self.make_user_anonymous()
809 action = self.form['__destination_url'].value
810 self.login(message=_('No such user "%(name)s"')%locals(),
811 action=action)
812 return 0
814 # and that the password is correct
815 pw = self.db.user.get(uid, 'password')
816 if password != pw:
817 self.make_user_anonymous()
818 action = self.form['__destination_url'].value
819 self.login(message=_('Incorrect password'), action=action)
820 return 0
822 self.set_cookie(self.user, password)
823 return 1
825 def newuser_action(self, message=None):
826 '''Attempt to create a new user based on the contents of the form
827 and then set the cookie.
829 return 1 on successful login
830 '''
831 # re-open the database as "admin"
832 self.db = self.instance.open('admin')
834 # TODO: pre-check the required fields and username key property
835 cl = self.db.user
836 try:
837 props, dummy = parsePropsFromForm(self.db, cl, self.form)
838 uid = cl.create(**props)
839 except ValueError, message:
840 action = self.form['__destination_url'].value
841 self.login(message, action=action)
842 return 0
843 self.user = cl.get(uid, 'username')
844 password = cl.get(uid, 'password')
845 self.set_cookie(self.user, self.form['password'].value)
846 return 1
848 def set_cookie(self, user, password):
849 # construct the cookie
850 user = binascii.b2a_base64('%s:%s'%(user, password)).strip()
851 if user[-1] == '=':
852 if user[-2] == '=':
853 user = user[:-2]
854 else:
855 user = user[:-1]
856 expire = Cookie._getdate(86400*365)
857 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME']))
858 self.header({'Set-Cookie': 'roundup_user=%s; expires=%s; Path=%s;' % (
859 user, expire, path)})
861 def make_user_anonymous(self):
862 # make us anonymous if we can
863 try:
864 self.db.user.lookup('anonymous')
865 self.user = 'anonymous'
866 except KeyError:
867 self.user = None
869 def logout(self, message=None):
870 self.make_user_anonymous()
871 # construct the logout cookie
872 now = Cookie._getdate()
873 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME']))
874 self.header({'Set-Cookie':
875 'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now,
876 path)})
877 self.login()
880 def main(self):
881 '''Wrap the database accesses so we can close the database cleanly
882 '''
883 # determine the uid to use
884 self.db = self.instance.open('admin')
885 cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
886 user = 'anonymous'
887 if (cookie.has_key('roundup_user') and
888 cookie['roundup_user'].value != 'deleted'):
889 cookie = cookie['roundup_user'].value
890 if len(cookie)%4:
891 cookie = cookie + '='*(4-len(cookie)%4)
892 try:
893 user, password = binascii.a2b_base64(cookie).split(':')
894 except (TypeError, binascii.Error, binascii.Incomplete):
895 # damaged cookie!
896 user, password = 'anonymous', ''
898 # make sure the user exists
899 try:
900 uid = self.db.user.lookup(user)
901 # now validate the password
902 if password != self.db.user.get(uid, 'password'):
903 user = 'anonymous'
904 except KeyError:
905 user = 'anonymous'
907 # make sure the anonymous user is valid if we're using it
908 if user == 'anonymous':
909 self.make_user_anonymous()
910 else:
911 self.user = user
913 # re-open the database for real, using the user
914 self.db = self.instance.open(self.user)
916 # now figure which function to call
917 path = self.split_path
919 # default action to index if the path has no information in it
920 if not path or path[0] in ('', 'index'):
921 action = 'index'
922 else:
923 action = path[0]
925 # Everthing ignores path[1:]
926 # - The file download link generator actually relies on this - it
927 # appends the name of the file to the URL so the download file name
928 # is correct, but doesn't actually use it.
930 # everyone is allowed to try to log in
931 if action == 'login_action':
932 # try to login
933 if not self.login_action():
934 return
935 # figure the resulting page
936 action = self.form['__destination_url'].value
937 if not action:
938 action = 'index'
939 self.do_action(action)
940 return
942 # allow anonymous people to register
943 if action == 'newuser_action':
944 # if we don't have a login and anonymous people aren't allowed to
945 # register, then spit up the login form
946 if self.ANONYMOUS_REGISTER == 'deny' and self.user is None:
947 if action == 'login':
948 self.login() # go to the index after login
949 else:
950 self.login(action=action)
951 return
952 # try to add the user
953 if not self.newuser_action():
954 return
955 # figure the resulting page
956 action = self.form['__destination_url'].value
957 if not action:
958 action = 'index'
960 # no login or registration, make sure totally anonymous access is OK
961 elif self.ANONYMOUS_ACCESS == 'deny' and self.user is None:
962 if action == 'login':
963 self.login() # go to the index after login
964 else:
965 self.login(action=action)
966 return
968 # just a regular action
969 self.do_action(action)
971 # commit all changes to the database
972 self.db.commit()
974 def do_action(self, action, dre=re.compile(r'([^\d]+)(\d+)'),
975 nre=re.compile(r'new(\w+)')):
976 '''Figure the user's action and do it.
977 '''
978 # here be the "normal" functionality
979 if action == 'index':
980 self.index()
981 return
982 if action == 'list_classes':
983 self.classes()
984 return
985 if action == 'login':
986 self.login()
987 return
988 if action == 'logout':
989 self.logout()
990 return
991 m = dre.match(action)
992 if m:
993 self.classname = m.group(1)
994 self.nodeid = m.group(2)
995 try:
996 cl = self.db.classes[self.classname]
997 except KeyError:
998 raise NotFound
999 try:
1000 cl.get(self.nodeid, 'id')
1001 except IndexError:
1002 raise NotFound
1003 try:
1004 func = getattr(self, 'show%s'%self.classname)
1005 except AttributeError:
1006 raise NotFound
1007 func()
1008 return
1009 m = nre.match(action)
1010 if m:
1011 self.classname = m.group(1)
1012 try:
1013 func = getattr(self, 'new%s'%self.classname)
1014 except AttributeError:
1015 raise NotFound
1016 func()
1017 return
1018 self.classname = action
1019 try:
1020 self.db.getclass(self.classname)
1021 except KeyError:
1022 raise NotFound
1023 self.list()
1026 class ExtendedClient(Client):
1027 '''Includes pages and page heading information that relate to the
1028 extended schema.
1029 '''
1030 showsupport = Client.shownode
1031 showtimelog = Client.shownode
1032 newsupport = Client.newnode
1033 newtimelog = Client.newnode
1035 default_index_sort = ['-activity']
1036 default_index_group = ['priority']
1037 default_index_filter = ['status']
1038 default_index_columns = ['activity','status','title','assignedto']
1039 default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
1041 def pagehead(self, title, message=None):
1042 url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
1043 machine = self.env['SERVER_NAME']
1044 port = self.env['SERVER_PORT']
1045 if port != '80': machine = machine + ':' + port
1046 base = urlparse.urlunparse(('http', machine, url, None, None, None))
1047 if message is not None:
1048 message = _('<div class="system-msg">%(message)s</div>')%locals()
1049 else:
1050 message = ''
1051 style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
1052 user_name = self.user or ''
1053 if self.user == 'admin':
1054 admin_links = _(' | <a href="list_classes">Class List</a>' \
1055 ' | <a href="user">User List</a>')
1056 else:
1057 admin_links = ''
1058 if self.user not in (None, 'anonymous'):
1059 userid = self.db.user.lookup(self.user)
1060 user_info = _('''
1061 <a href="issue?assignedto=%(userid)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> |
1062 <a href="support?assignedto=%(userid)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> |
1063 <a href="user%(userid)s">My Details</a> | <a href="logout">Logout</a>
1064 ''')%locals()
1065 else:
1066 user_info = _('<a href="login">Login</a>')
1067 if self.user is not None:
1068 if self.user == 'admin':
1069 add_links = _('''
1070 | Add
1071 <a href="newissue">Issue</a>,
1072 <a href="newsupport">Support</a>,
1073 <a href="newuser">User</a>
1074 ''')
1075 else:
1076 add_links = _('''
1077 | Add
1078 <a href="newissue">Issue</a>,
1079 <a href="newsupport">Support</a>,
1080 ''')
1081 else:
1082 add_links = ''
1083 self.write(_('''<html><head>
1084 <title>%(title)s</title>
1085 <style type="text/css">%(style)s</style>
1086 </head>
1087 <body bgcolor=#ffffff>
1088 %(message)s
1089 <table width=100%% border=0 cellspacing=0 cellpadding=2>
1090 <tr class="location-bar"><td><big><strong>%(title)s</strong></big></td>
1091 <td align=right valign=bottom>%(user_name)s</td></tr>
1092 <tr class="location-bar">
1093 <td align=left>All
1094 <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>,
1095 <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>
1096 | Unassigned
1097 <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>,
1098 <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>
1099 %(add_links)s
1100 %(admin_links)s</td>
1101 <td align=right>%(user_info)s</td>
1102 </table>
1103 ''')%locals())
1105 def parsePropsFromForm(db, cl, form, nodeid=0):
1106 '''Pull properties for the given class out of the form.
1107 '''
1108 props = {}
1109 changed = {}
1110 keys = form.keys()
1111 num_re = re.compile('^\d+$')
1112 for key in keys:
1113 if not cl.properties.has_key(key):
1114 continue
1115 proptype = cl.properties[key]
1116 if isinstance(proptype, hyperdb.String):
1117 value = form[key].value.strip()
1118 elif isinstance(proptype, hyperdb.Password):
1119 value = password.Password(form[key].value.strip())
1120 elif isinstance(proptype, hyperdb.Date):
1121 value = date.Date(form[key].value.strip())
1122 elif isinstance(proptype, hyperdb.Interval):
1123 value = date.Interval(form[key].value.strip())
1124 elif isinstance(proptype, hyperdb.Link):
1125 value = form[key].value.strip()
1126 # see if it's the "no selection" choice
1127 if value == '-1':
1128 # don't set this property
1129 continue
1130 else:
1131 # handle key values
1132 link = cl.properties[key].classname
1133 if not num_re.match(value):
1134 try:
1135 value = db.classes[link].lookup(value)
1136 except KeyError:
1137 raise ValueError, _('property "%(propname)s": '
1138 '%(value)s not a %(classname)s')%{'propname':key,
1139 'value': value, 'classname': link}
1140 elif isinstance(proptype, hyperdb.Multilink):
1141 value = form[key]
1142 if type(value) != type([]):
1143 value = [i.strip() for i in value.value.split(',')]
1144 else:
1145 value = [i.value.strip() for i in value]
1146 link = cl.properties[key].classname
1147 l = []
1148 for entry in map(str, value):
1149 if entry == '': continue
1150 if not num_re.match(entry):
1151 try:
1152 entry = db.classes[link].lookup(entry)
1153 except KeyError:
1154 raise ValueError, _('property "%(propname)s": '
1155 '"%(value)s" not an entry of %(classname)s')%{
1156 'propname':key, 'value': entry, 'classname': link}
1157 l.append(entry)
1158 l.sort()
1159 value = l
1160 props[key] = value
1162 # get the old value
1163 if nodeid:
1164 try:
1165 existing = cl.get(nodeid, key)
1166 except KeyError:
1167 # this might be a new property for which there is no existing
1168 # value
1169 if not cl.properties.has_key(key): raise
1171 # if changed, set it
1172 if nodeid and value != existing:
1173 changed[key] = value
1174 props[key] = value
1175 return props, changed
1177 #
1178 # $Log: not supported by cvs2svn $
1179 # Revision 1.85 2001/12/20 06:13:24 rochecompaan
1180 # Bugs fixed:
1181 # . Exception handling in hyperdb for strings-that-look-like numbers got
1182 # lost somewhere
1183 # . Internet Explorer submits full path for filename - we now strip away
1184 # the path
1185 # Features added:
1186 # . Link and multilink properties are now displayed sorted in the cgi
1187 # interface
1188 #
1189 # Revision 1.84 2001/12/18 15:30:30 rochecompaan
1190 # Fixed bugs:
1191 # . Fixed file creation and retrieval in same transaction in anydbm
1192 # backend
1193 # . Cgi interface now renders new issue after issue creation
1194 # . Could not set issue status to resolved through cgi interface
1195 # . Mail gateway was changing status back to 'chatting' if status was
1196 # omitted as an argument
1197 #
1198 # Revision 1.83 2001/12/15 23:51:01 richard
1199 # Tested the changes and fixed a few problems:
1200 # . files are now attached to the issue as well as the message
1201 # . newuser is a real method now since we don't want to do the message/file
1202 # stuff for it
1203 # . added some documentation
1204 # The really big changes in the diff are a result of me moving some code
1205 # around to keep like methods together a bit better.
1206 #
1207 # Revision 1.82 2001/12/15 19:24:39 rochecompaan
1208 # . Modified cgi interface to change properties only once all changes are
1209 # collected, files created and messages generated.
1210 # . Moved generation of change note to nosyreactors.
1211 # . We now check for changes to "assignedto" to ensure it's added to the
1212 # nosy list.
1213 #
1214 # Revision 1.81 2001/12/12 23:55:00 richard
1215 # Fixed some problems with user editing
1216 #
1217 # Revision 1.80 2001/12/12 23:27:14 richard
1218 # Added a Zope frontend for roundup.
1219 #
1220 # Revision 1.79 2001/12/10 22:20:01 richard
1221 # Enabled transaction support in the bsddb backend. It uses the anydbm code
1222 # where possible, only replacing methods where the db is opened (it uses the
1223 # btree opener specifically.)
1224 # Also cleaned up some change note generation.
1225 # Made the backends package work with pydoc too.
1226 #
1227 # Revision 1.78 2001/12/07 05:59:27 rochecompaan
1228 # Fixed small bug that prevented adding issues through the web.
1229 #
1230 # Revision 1.77 2001/12/06 22:48:29 richard
1231 # files multilink was being nuked in post_edit_node
1232 #
1233 # Revision 1.76 2001/12/05 14:26:44 rochecompaan
1234 # Removed generation of change note from "sendmessage" in roundupdb.py.
1235 # The change note is now generated when the message is created.
1236 #
1237 # Revision 1.75 2001/12/04 01:25:08 richard
1238 # Added some rollbacks where we were catching exceptions that would otherwise
1239 # have stopped committing.
1240 #
1241 # Revision 1.74 2001/12/02 05:06:16 richard
1242 # . We now use weakrefs in the Classes to keep the database reference, so
1243 # the close() method on the database is no longer needed.
1244 # I bumped the minimum python requirement up to 2.1 accordingly.
1245 # . #487480 ] roundup-server
1246 # . #487476 ] INSTALL.txt
1247 #
1248 # I also cleaned up the change message / post-edit stuff in the cgi client.
1249 # There's now a clearly marked "TODO: append the change note" where I believe
1250 # the change note should be added there. The "changes" list will obviously
1251 # have to be modified to be a dict of the changes, or somesuch.
1252 #
1253 # More testing needed.
1254 #
1255 # Revision 1.73 2001/12/01 07:17:50 richard
1256 # . We now have basic transaction support! Information is only written to
1257 # the database when the commit() method is called. Only the anydbm
1258 # backend is modified in this way - neither of the bsddb backends have been.
1259 # The mail, admin and cgi interfaces all use commit (except the admin tool
1260 # doesn't have a commit command, so interactive users can't commit...)
1261 # . Fixed login/registration forwarding the user to the right page (or not,
1262 # on a failure)
1263 #
1264 # Revision 1.72 2001/11/30 20:47:58 rochecompaan
1265 # Links in page header are now consistent with default sort order.
1266 #
1267 # Fixed bugs:
1268 # - When login failed the list of issues were still rendered.
1269 # - User was redirected to index page and not to his destination url
1270 # if his first login attempt failed.
1271 #
1272 # Revision 1.71 2001/11/30 20:28:10 rochecompaan
1273 # Property changes are now completely traceable, whether changes are
1274 # made through the web or by email
1275 #
1276 # Revision 1.70 2001/11/30 00:06:29 richard
1277 # Converted roundup/cgi_client.py to use _()
1278 # Added the status file, I18N_PROGRESS.txt
1279 #
1280 # Revision 1.69 2001/11/29 23:19:51 richard
1281 # Removed the "This issue has been edited through the web" when a valid
1282 # change note is supplied.
1283 #
1284 # Revision 1.68 2001/11/29 04:57:23 richard
1285 # a little comment
1286 #
1287 # Revision 1.67 2001/11/28 21:55:35 richard
1288 # . login_action and newuser_action return values were being ignored
1289 # . Woohoo! Found that bloody re-login bug that was killing the mail
1290 # gateway.
1291 # (also a minor cleanup in hyperdb)
1292 #
1293 # Revision 1.66 2001/11/27 03:00:50 richard
1294 # couple of bugfixes from latest patch integration
1295 #
1296 # Revision 1.65 2001/11/26 23:00:53 richard
1297 # This config stuff is getting to be a real mess...
1298 #
1299 # Revision 1.64 2001/11/26 22:56:35 richard
1300 # typo
1301 #
1302 # Revision 1.63 2001/11/26 22:55:56 richard
1303 # Feature:
1304 # . Added INSTANCE_NAME to configuration - used in web and email to identify
1305 # the instance.
1306 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1307 # signature info in e-mails.
1308 # . Some more flexibility in the mail gateway and more error handling.
1309 # . Login now takes you to the page you back to the were denied access to.
1310 #
1311 # Fixed:
1312 # . Lots of bugs, thanks Roché and others on the devel mailing list!
1313 #
1314 # Revision 1.62 2001/11/24 00:45:42 jhermann
1315 # typeof() instead of type(): avoid clash with database field(?) "type"
1316 #
1317 # Fixes this traceback:
1318 #
1319 # Traceback (most recent call last):
1320 # File "roundup\cgi_client.py", line 535, in newnode
1321 # self._post_editnode(nid)
1322 # File "roundup\cgi_client.py", line 415, in _post_editnode
1323 # if type(value) != type([]): value = [value]
1324 # UnboundLocalError: local variable 'type' referenced before assignment
1325 #
1326 # Revision 1.61 2001/11/22 15:46:42 jhermann
1327 # Added module docstrings to all modules.
1328 #
1329 # Revision 1.60 2001/11/21 22:57:28 jhermann
1330 # Added dummy hooks for I18N and some preliminary (test) markup of
1331 # translatable messages
1332 #
1333 # Revision 1.59 2001/11/21 03:21:13 richard
1334 # oops
1335 #
1336 # Revision 1.58 2001/11/21 03:11:28 richard
1337 # Better handling of new properties.
1338 #
1339 # Revision 1.57 2001/11/15 10:24:27 richard
1340 # handle the case where there is no file attached
1341 #
1342 # Revision 1.56 2001/11/14 21:35:21 richard
1343 # . users may attach files to issues (and support in ext) through the web now
1344 #
1345 # Revision 1.55 2001/11/07 02:34:06 jhermann
1346 # Handling of damaged login cookies
1347 #
1348 # Revision 1.54 2001/11/07 01:16:12 richard
1349 # Remove the '=' padding from cookie value so quoting isn't an issue.
1350 #
1351 # Revision 1.53 2001/11/06 23:22:05 jhermann
1352 # More IE fixes: it does not like quotes around cookie values; in the
1353 # hope this does not break anything for other browser; if it does, we
1354 # need to check HTTP_USER_AGENT
1355 #
1356 # Revision 1.52 2001/11/06 23:11:22 jhermann
1357 # Fixed debug output in page footer; added expiry date to the login cookie
1358 # (expires 1 year in the future) to prevent probs with certain versions
1359 # of IE
1360 #
1361 # Revision 1.51 2001/11/06 22:00:34 jhermann
1362 # Get debug level from ROUNDUP_DEBUG env var
1363 #
1364 # Revision 1.50 2001/11/05 23:45:40 richard
1365 # Fixed newuser_action so it sets the cookie with the unencrypted password.
1366 # Also made it present nicer error messages (not tracebacks).
1367 #
1368 # Revision 1.49 2001/11/04 03:07:12 richard
1369 # Fixed various cookie-related bugs:
1370 # . bug #477685 ] base64.decodestring breaks
1371 # . bug #477837 ] lynx does not like the cookie
1372 # . bug #477892 ] Password edit doesn't fix login cookie
1373 # Also closed a security hole - a logged-in user could edit another user's
1374 # details.
1375 #
1376 # Revision 1.48 2001/11/03 01:30:18 richard
1377 # Oops. uses pagefoot now.
1378 #
1379 # Revision 1.47 2001/11/03 01:29:28 richard
1380 # Login page didn't have all close tags.
1381 #
1382 # Revision 1.46 2001/11/03 01:26:55 richard
1383 # possibly fix truncated base64'ed user:pass
1384 #
1385 # Revision 1.45 2001/11/01 22:04:37 richard
1386 # Started work on supporting a pop3-fetching server
1387 # Fixed bugs:
1388 # . bug #477104 ] HTML tag error in roundup-server
1389 # . bug #477107 ] HTTP header problem
1390 #
1391 # Revision 1.44 2001/10/28 23:03:08 richard
1392 # Added more useful header to the classic schema.
1393 #
1394 # Revision 1.43 2001/10/24 00:01:42 richard
1395 # More fixes to lockout logic.
1396 #
1397 # Revision 1.42 2001/10/23 23:56:03 richard
1398 # HTML typo
1399 #
1400 # Revision 1.41 2001/10/23 23:52:35 richard
1401 # Fixed lock-out logic, thanks Roch'e for pointing out the problems.
1402 #
1403 # Revision 1.40 2001/10/23 23:06:39 richard
1404 # Some cleanup.
1405 #
1406 # Revision 1.39 2001/10/23 01:00:18 richard
1407 # Re-enabled login and registration access after lopping them off via
1408 # disabling access for anonymous users.
1409 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1410 # a couple of bugs while I was there. Probably introduced a couple, but
1411 # things seem to work OK at the moment.
1412 #
1413 # Revision 1.38 2001/10/22 03:25:01 richard
1414 # Added configuration for:
1415 # . anonymous user access and registration (deny/allow)
1416 # . filter "widget" location on index page (top, bottom, both)
1417 # Updated some documentation.
1418 #
1419 # Revision 1.37 2001/10/21 07:26:35 richard
1420 # feature #473127: Filenames. I modified the file.index and htmltemplate
1421 # source so that the filename is used in the link and the creation
1422 # information is displayed.
1423 #
1424 # Revision 1.36 2001/10/21 04:44:50 richard
1425 # bug #473124: UI inconsistency with Link fields.
1426 # This also prompted me to fix a fairly long-standing usability issue -
1427 # that of being able to turn off certain filters.
1428 #
1429 # Revision 1.35 2001/10/21 00:17:54 richard
1430 # CGI interface view customisation section may now be hidden (patch from
1431 # Roch'e Compaan.)
1432 #
1433 # Revision 1.34 2001/10/20 11:58:48 richard
1434 # Catch errors in login - no username or password supplied.
1435 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
1436 #
1437 # Revision 1.33 2001/10/17 00:18:41 richard
1438 # Manually constructing cookie headers now.
1439 #
1440 # Revision 1.32 2001/10/16 03:36:21 richard
1441 # CGI interface wasn't handling checkboxes at all.
1442 #
1443 # Revision 1.31 2001/10/14 10:55:00 richard
1444 # Handle empty strings in HTML template Link function
1445 #
1446 # Revision 1.30 2001/10/09 07:38:58 richard
1447 # Pushed the base code for the extended schema CGI interface back into the
1448 # code cgi_client module so that future updates will be less painful.
1449 # Also removed a debugging print statement from cgi_client.
1450 #
1451 # Revision 1.29 2001/10/09 07:25:59 richard
1452 # Added the Password property type. See "pydoc roundup.password" for
1453 # implementation details. Have updated some of the documentation too.
1454 #
1455 # Revision 1.28 2001/10/08 00:34:31 richard
1456 # Change message was stuffing up for multilinks with no key property.
1457 #
1458 # Revision 1.27 2001/10/05 02:23:24 richard
1459 # . roundup-admin create now prompts for property info if none is supplied
1460 # on the command-line.
1461 # . hyperdb Class getprops() method may now return only the mutable
1462 # properties.
1463 # . Login now uses cookies, which makes it a whole lot more flexible. We can
1464 # now support anonymous user access (read-only, unless there's an
1465 # "anonymous" user, in which case write access is permitted). Login
1466 # handling has been moved into cgi_client.Client.main()
1467 # . The "extended" schema is now the default in roundup init.
1468 # . The schemas have had their page headings modified to cope with the new
1469 # login handling. Existing installations should copy the interfaces.py
1470 # file from the roundup lib directory to their instance home.
1471 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
1472 # Ping - has been removed.
1473 # . Fixed a whole bunch of places in the CGI interface where we should have
1474 # been returning Not Found instead of throwing an exception.
1475 # . Fixed a deviation from the spec: trying to modify the 'id' property of
1476 # an item now throws an exception.
1477 #
1478 # Revision 1.26 2001/09/12 08:31:42 richard
1479 # handle cases where mime type is not guessable
1480 #
1481 # Revision 1.25 2001/08/29 05:30:49 richard
1482 # change messages weren't being saved when there was no-one on the nosy list.
1483 #
1484 # Revision 1.24 2001/08/29 04:49:39 richard
1485 # didn't clean up fully after debugging :(
1486 #
1487 # Revision 1.23 2001/08/29 04:47:18 richard
1488 # Fixed CGI client change messages so they actually include the properties
1489 # changed (again).
1490 #
1491 # Revision 1.22 2001/08/17 00:08:10 richard
1492 # reverted back to sending messages always regardless of who is doing the web
1493 # edit. change notes weren't being saved. bleah. hackish.
1494 #
1495 # Revision 1.21 2001/08/15 23:43:18 richard
1496 # Fixed some isFooTypes that I missed.
1497 # Refactored some code in the CGI code.
1498 #
1499 # Revision 1.20 2001/08/12 06:32:36 richard
1500 # using isinstance(blah, Foo) now instead of isFooType
1501 #
1502 # Revision 1.19 2001/08/07 00:24:42 richard
1503 # stupid typo
1504 #
1505 # Revision 1.18 2001/08/07 00:15:51 richard
1506 # Added the copyright/license notice to (nearly) all files at request of
1507 # Bizar Software.
1508 #
1509 # Revision 1.17 2001/08/02 06:38:17 richard
1510 # Roundupdb now appends "mailing list" information to its messages which
1511 # include the e-mail address and web interface address. Templates may
1512 # override this in their db classes to include specific information (support
1513 # instructions, etc).
1514 #
1515 # Revision 1.16 2001/08/02 05:55:25 richard
1516 # Web edit messages aren't sent to the person who did the edit any more. No
1517 # message is generated if they are the only person on the nosy list.
1518 #
1519 # Revision 1.15 2001/08/02 00:34:10 richard
1520 # bleah syntax error
1521 #
1522 # Revision 1.14 2001/08/02 00:26:16 richard
1523 # Changed the order of the information in the message generated by web edits.
1524 #
1525 # Revision 1.13 2001/07/30 08:12:17 richard
1526 # Added time logging and file uploading to the templates.
1527 #
1528 # Revision 1.12 2001/07/30 06:26:31 richard
1529 # Added some documentation on how the newblah works.
1530 #
1531 # Revision 1.11 2001/07/30 06:17:45 richard
1532 # Features:
1533 # . Added ability for cgi newblah forms to indicate that the new node
1534 # should be linked somewhere.
1535 # Fixed:
1536 # . Fixed the agument handling for the roundup-admin find command.
1537 # . Fixed handling of summary when no note supplied for newblah. Again.
1538 # . Fixed detection of no form in htmltemplate Field display.
1539 #
1540 # Revision 1.10 2001/07/30 02:37:34 richard
1541 # Temporary measure until we have decent schema migration...
1542 #
1543 # Revision 1.9 2001/07/30 01:25:07 richard
1544 # Default implementation is now "classic" rather than "extended" as one would
1545 # expect.
1546 #
1547 # Revision 1.8 2001/07/29 08:27:40 richard
1548 # Fixed handling of passed-in values in form elements (ie. during a
1549 # drill-down)
1550 #
1551 # Revision 1.7 2001/07/29 07:01:39 richard
1552 # Added vim command to all source so that we don't get no steenkin' tabs :)
1553 #
1554 # Revision 1.6 2001/07/29 04:04:00 richard
1555 # Moved some code around allowing for subclassing to change behaviour.
1556 #
1557 # Revision 1.5 2001/07/28 08:16:52 richard
1558 # New issue form handles lack of note better now.
1559 #
1560 # Revision 1.4 2001/07/28 00:34:34 richard
1561 # Fixed some non-string node ids.
1562 #
1563 # Revision 1.3 2001/07/23 03:56:30 richard
1564 # oops, missed a config removal
1565 #
1566 # Revision 1.2 2001/07/22 12:09:32 richard
1567 # Final commit of Grande Splite
1568 #
1569 # Revision 1.1 2001/07/22 11:58:35 richard
1570 # More Grande Splite
1571 #
1572 #
1573 # vim: set filetype=python ts=4 sw=4 et si