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