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