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