X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fcgi_client.py;h=86177bb6b257e0978b35d5d4970cb982d5e404f7;hb=1b8dd00d357bfc90e4f1d57ac8c3dd36aa9a0ccd;hp=c565c546d0823757bb23b5fe952cee1ad106c61b;hpb=27e2c6ec318fec6ad7d874b0ee17ff460b5e33e0;p=roundup.git diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py index c565c54..86177bb 100644 --- a/roundup/cgi_client.py +++ b/roundup/cgi_client.py @@ -15,12 +15,17 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: cgi_client.py,v 1.49 2001-11-04 03:07:12 richard Exp $ +# $Id: cgi_client.py,v 1.90 2002-01-08 03:56:55 richard Exp $ + +__doc__ = """ +WWW request handler (also used in the stand-alone server). +""" import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes -import binascii, Cookie, time +import binascii, Cookie, time, random import roundupdb, htmltemplate, date, hyperdb, password +from roundup.i18n import _ class Unauthorised(ValueError): pass @@ -47,21 +52,31 @@ class Client: ANONYMOUS_ACCESS - one of 'deny', 'allow' ANONYMOUS_REGISTER - one of 'deny', 'allow' + from the roundup class: + INSTANCE_NAME - defaults to 'Roundup issue tracker' + ''' FILTER_POSITION = 'bottom' # one of 'top', 'bottom', 'top and bottom' ANONYMOUS_ACCESS = 'deny' # one of 'deny', 'allow' ANONYMOUS_REGISTER = 'deny' # one of 'deny', 'allow' - def __init__(self, instance, request, env): + def __init__(self, instance, request, env, form=None): self.instance = instance self.request = request self.env = env self.path = env['PATH_INFO'] self.split_path = self.path.split('/') - self.form = cgi.FieldStorage(environ=env) + if form is None: + self.form = cgi.FieldStorage(environ=env) + else: + self.form = form self.headers_done = 0 - self.debug = 0 + try: + self.debug = int(env.get("ROUNDUP_DEBUG", 0)) + except ValueError: + # someone gave us a non-int debug level, turn it off + self.debug = 0 def getuid(self): return self.db.user.lookup(self.user) @@ -76,6 +91,8 @@ class Client: self.request.send_header(*entry) self.request.end_headers() self.headers_done = 1 + if self.debug: + self.headers_sent = headers def pagehead(self, title, message=None): url = self.env['SCRIPT_NAME'] + '/' @@ -84,70 +101,78 @@ class Client: if port != '80': machine = machine + ':' + port base = urlparse.urlunparse(('http', machine, url, None, None, None)) if message is not None: - message = '
%s
'%message + message = _('
%(message)s
')%locals() else: message = '' style = open(os.path.join(self.TEMPLATES, 'style.css')).read() user_name = self.user or '' if self.user == 'admin': - admin_links = ' | Class List' + admin_links = _(' | Class List' \ + ' | User List' \ + ' | Add User') else: admin_links = '' if self.user not in (None, 'anonymous'): userid = self.db.user.lookup(self.user) - user_info = ''' -My Issues | -My Details | Logout -'''%(userid, userid) + user_info = _(''' +My Issues | +My Details | Logout +''')%locals() else: - user_info = 'Login' + user_info = _('Login') if self.user is not None: - add_links = ''' + add_links = _(''' | Add -Issue, -User -''' +Issue +''') else: add_links = '' - self.write(''' -%s - + self.write(_(''' +%(title)s + -%s +%(message)s - - + + - +Issues +%(add_links)s +%(admin_links)s +
%s%s
%(title)s%(user_name)s
All -Issues +Issues | Unassigned -Issues -%s -%s%s%(user_info)s
-'''%(title, style, message, title, user_name, add_links, admin_links, - user_info)) +''')%locals()) def pagefoot(self): if self.debug: - self.write('
') - self.write('
Path
') + self.write(_('
Path
')) self.write('
%s
'%(', '.join(map(repr, self.split_path)))) keys = self.form.keys() keys.sort() if keys: - self.write('
Form entries
') + self.write(_('
Form entries
')) for k in self.form.keys(): - v = str(self.form[k].value) - self.write('
%s:%s
'%(k, cgi.escape(v))) + v = self.form.getvalue(k, "") + if type(v) is type([]): + # Multiple username fields specified + v = "|".join(v) + self.write('
%s=%s
'%(k, cgi.escape(v))) + keys = self.headers_sent.keys() + keys.sort() + self.write(_('
Sent these HTTP headers
')) + for k in keys: + v = self.headers_sent[k] + self.write('
%s=%s
'%(k, cgi.escape(v))) keys = self.env.keys() keys.sort() - self.write('
CGI environment
') + self.write(_('
CGI environment
')) for k in keys: v = self.env[k] - self.write('
%s:%s
'%(k, cgi.escape(v))) + self.write('
%s=%s
'%(k, cgi.escape(v))) self.write('
') self.write('') @@ -259,7 +284,9 @@ class Client: ''' cn = self.classname - self.pagehead('Index of %s'%cn) + cl = self.db.classes[cn] + self.pagehead(_('%(instancename)s: Index of %(classname)s')%{ + 'classname': cn, 'instancename': self.INSTANCE_NAME}) if sort is None: sort = self.index_arg(':sort') if group is None: group = self.index_arg(':group') if filter is None: filter = self.index_arg(':filter') @@ -282,15 +309,27 @@ class Client: # possibly perform an edit keys = self.form.keys() num_re = re.compile('^\d+$') - if keys: + # don't try to set properties if the user has just logged in + if keys and not self.form.has_key('__login_name'): try: props, changed = parsePropsFromForm(self.db, cl, self.form, self.nodeid) - cl.set(self.nodeid, **props) - self._post_editnode(self.nodeid, changed) + # make changes to the node + self._changenode(props) + # handle linked nodes + self._post_editnode(self.nodeid) # and some nice feedback for the user - message = '%s edited ok'%', '.join(changed) + if changed: + message = _('%(changes)s edited ok')%{'changes': + ', '.join(changed.keys())} + elif self.form.has_key('__note') and self.form['__note'].value: + message = _('note added') + elif self.form.has_key('__file'): + message = _('file added') + else: + message = _('nothing changed') except: + self.db.rollback() s = StringIO.StringIO() traceback.print_exc(None, s) message = '
%s
'%cgi.escape(s.getvalue()) @@ -311,79 +350,152 @@ class Client: showissue = shownode showmsg = shownode - def showuser(self, message=None): - '''Display a user page for editing. Make sure the user is allowed - to edit this node, and also check for password changes. + def _add_assignedto_to_nosy(self, props): + ''' add the assignedto value from the props to the nosy list ''' - if self.user == 'anonymous': - raise Unauthorised - - user = self.db.user - - # get the username of the node being edited - node_user = user.get(self.nodeid, 'username') - - if self.user not in ('admin', node_user): - raise Unauthorised - - # - # perform any editing - # - keys = self.form.keys() - num_re = re.compile('^\d+$') - if keys: - try: - props, changed = parsePropsFromForm(self.db, user, self.form, - self.nodeid) - if self.nodeid == self.getuid() and 'password' in changed: - set_cookie = self.form['password'].value.strip() - else: - set_cookie = 0 - user.set(self.nodeid, **props) - self._post_editnode(self.nodeid, changed) - # and some feedback for the user - message = '%s edited ok'%', '.join(changed) - except: - s = StringIO.StringIO() - traceback.print_exc(None, s) - message = '
%s
'%cgi.escape(s.getvalue()) + if not props.has_key('assignedto'): + return + assignedto_id = props['assignedto'] + if props.has_key('nosy') and assignedto_id not in props['nosy']: + props['nosy'].append(assignedto_id) else: - set_cookie = 0 - - # fix the cookie if the password has changed - if set_cookie: - self.set_cookie(self.user, set_cookie) + props['nosy'] = cl.get(self.nodeid, 'nosy') + props['nosy'].append(assignedto_id) - # - # now the display - # - self.pagehead('User: %s'%node_user, message) + def _changenode(self, props): + ''' change the node based on the contents of the form + ''' + cl = self.db.classes[self.classname] + # set status to chatting if 'unread' or 'resolved' + try: + # determine the id of 'unread','resolved' and 'chatting' + unread_id = self.db.status.lookup('unread') + resolved_id = self.db.status.lookup('resolved') + chatting_id = self.db.status.lookup('chatting') + current_status = cl.get(self.nodeid, 'status') + if props.has_key('status'): + new_status = props['status'] + else: + # apparently there's a chance that some browsers don't + # send status... + new_status = current_status + except KeyError: + pass + else: + if new_status == unread_id or (new_status == resolved_id + and current_status == resolved_id): + props['status'] = chatting_id - # use the template to display the item - item = htmltemplate.ItemTemplate(self, self.TEMPLATES, 'user') - item.render(self.nodeid) - self.pagefoot() + self._add_assignedto_to_nosy(props) - def showfile(self): - ''' display a file - ''' - nodeid = self.nodeid - cl = self.db.file - type = cl.get(nodeid, 'type') - if type == 'message/rfc822': - type = 'text/plain' - self.header(headers={'Content-Type': type}) - self.write(cl.get(nodeid, 'content')) + # create the message + message, files = self._handle_message() + if message: + props['messages'] = cl.get(self.nodeid, 'messages') + [message] + if files: + props['files'] = cl.get(self.nodeid, 'files') + files + # make the changes + cl.set(self.nodeid, **props) def _createnode(self): ''' create a node based on the contents of the form ''' cl = self.db.classes[self.classname] props, dummy = parsePropsFromForm(self.db, cl, self.form) + + # set status to 'unread' if not specified - a status of '- no + # selection -' doesn't make sense + if not props.has_key('status'): + try: + unread_id = self.db.status.lookup('unread') + except KeyError: + pass + else: + props['status'] = unread_id + + self._add_assignedto_to_nosy(props) + + # check for messages and files + message, files = self._handle_message() + if message: + props['messages'] = [message] + if files: + props['files'] = files + # create the node and return it's id return cl.create(**props) - def _post_editnode(self, nid, changes=None): - ''' do the linking and message sending part of the node creation + def _handle_message(self): + ''' generate and edit message + ''' + # handle file attachments + files = [] + if self.form.has_key('__file'): + file = self.form['__file'] + if file.filename: + filename = file.filename.split('\\')[-1] + mime_type = mimetypes.guess_type(filename)[0] + if not mime_type: + mime_type = "application/octet-stream" + # create the new file entry + files.append(self.db.file.create(type=mime_type, + name=filename, content=file.file.read())) + + # we don't want to do a message if none of the following is true... + cn = self.classname + cl = self.db.classes[self.classname] + props = cl.getprops() + note = None + # in a nutshell, don't do anything if there's no note or there's no + # NOSY + if self.form.has_key('__note'): + note = self.form['__note'].value + if not props.has_key('messages'): + return None, files + if not isinstance(props['messages'], hyperdb.Multilink): + return None, files + if not props['messages'].classname == 'msg': + return None, files + if not (self.form.has_key('nosy') or note): + return None, files + + # handle the note + if note: + if '\n' in note: + summary = re.split(r'\n\r?', note)[0] + else: + summary = note + m = ['%s\n'%note] + elif not files: + # don't generate a useless message + return None, files + + # handle the messageid + # TODO: handle inreplyto + messageid = "%s.%s.%s-%s"%(time.time(), random.random(), + self.classname, self.MAIL_DOMAIN) + + # now create the message, attaching the files + content = '\n'.join(m) + message_id = self.db.msg.create(author=self.getuid(), + recipients=[], date=date.Date('.'), summary=summary, + content=content, files=files, messageid=messageid) + + # update the messages property + return message_id, files + + def _post_editnode(self, nid): + '''Do the linking part of the node creation. + + If a form element has :link or :multilink appended to it, its + value specifies a node designator and the property on that node + to add _this_ node to as a link or multilink. + + This is typically used on, eg. the file upload page to indicated + which issue to link the file to. + + TODO: I suspect that this and newfile will go away now that + there's the ability to upload a file using the issue __file form + element! ''' cn = self.classname cl = self.db.classes[cn] @@ -409,66 +521,6 @@ class Client: link = self.db.classes[link] link.set(nodeid, **{property: nid}) - # generate an edit message - # don't bother if there's no messages or nosy list - props = cl.getprops() - note = None - if self.form.has_key('__note'): - note = self.form['__note'] - note = note.value - send = len(cl.get(nid, 'nosy', [])) or note - if (send and props.has_key('messages') and - isinstance(props['messages'], hyperdb.Multilink) and - props['messages'].classname == 'msg'): - - # handle the note - if note: - if '\n' in note: - summary = re.split(r'\n\r?', note)[0] - else: - summary = note - m = ['%s\n'%note] - else: - summary = 'This %s has been edited through the web.\n'%cn - m = [summary] - - first = 1 - for name, prop in props.items(): - if changes is not None and name not in changes: continue - if first: - m.append('\n-------') - first = 0 - value = cl.get(nid, name, None) - if isinstance(prop, hyperdb.Link): - link = self.db.classes[prop.classname] - key = link.labelprop(default_to_id=1) - if value is not None and key: - value = link.get(value, key) - else: - value = '-' - elif isinstance(prop, hyperdb.Multilink): - if value is None: value = [] - l = [] - link = self.db.classes[prop.classname] - key = link.labelprop(default_to_id=1) - for entry in value: - if key: - l.append(link.get(entry, key)) - else: - l.append(entry) - value = ', '.join(l) - m.append('%s: %s'%(name, value)) - - # now create the message - content = '\n'.join(m) - message_id = self.db.msg.create(author=self.getuid(), - recipients=[], date=date.Date('.'), summary=summary, - content=content) - messages = cl.get(nid, 'messages') - messages.append(message_id) - props = {'messages': messages} - cl.set(nid, **props) - def newnode(self, message=None): ''' Add a new node to the database. @@ -500,14 +552,28 @@ class Client: props = {} try: nid = self._createnode() + # handle linked nodes self._post_editnode(nid) # and some nice feedback for the user - message = '%s created ok'%cn + message = _('%(classname)s created ok')%{'classname': cn} + + # render the newly created issue + self.db.commit() + self.nodeid = nid + self.pagehead('%s: %s'%(self.classname.capitalize(), nid), + message) + item = htmltemplate.ItemTemplate(self, self.TEMPLATES, + self.classname) + item.render(nid) + self.pagefoot() + return except: + self.db.rollback() s = StringIO.StringIO() traceback.print_exc(None, s) message = '
%s
'%cgi.escape(s.getvalue()) - self.pagehead('New %s'%self.classname.capitalize(), message) + self.pagehead(_('New %(classname)s')%{'classname': + self.classname.capitalize()}, message) # call the template newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES, @@ -516,7 +582,39 @@ class Client: self.pagefoot() newissue = newnode - newuser = newnode + + def newuser(self, message=None): + ''' Add a new user to the database. + + Don't do any of the message or file handling, just create the node. + ''' + cn = self.classname + cl = self.db.classes[cn] + + # possibly perform a create + keys = self.form.keys() + if [i for i in keys if i[0] != ':']: + try: + props, dummy = parsePropsFromForm(self.db, cl, self.form) + nid = cl.create(**props) + # handle linked nodes + self._post_editnode(nid) + # and some nice feedback for the user + message = _('%(classname)s created ok')%{'classname': cn} + except: + self.db.rollback() + s = StringIO.StringIO() + traceback.print_exc(None, s) + message = '
%s
'%cgi.escape(s.getvalue()) + self.pagehead(_('New %(classname)s')%{'classname': + self.classname.capitalize()}, message) + + # call the template + newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES, + self.classname) + newitem.render(self.form) + + self.pagefoot() def newfile(self, message=None): ''' Add a new file to the database. @@ -532,29 +630,104 @@ class Client: if [i for i in keys if i[0] != ':']: try: file = self.form['content'] - type = mimetypes.guess_type(file.filename)[0] - if not type: - type = "application/octet-stream" - self._post_editnode(cl.create(content=file.file.read(), - type=type, name=file.filename)) + mime_type = mimetypes.guess_type(file.filename)[0] + if not mime_type: + mime_type = "application/octet-stream" + # save the file + nid = cl.create(content=file.file.read(), type=mime_type, + name=file.filename) + # handle linked nodes + self._post_editnode(nid) # and some nice feedback for the user - message = '%s created ok'%cn + message = _('%(classname)s created ok')%{'classname': cn} except: + self.db.rollback() s = StringIO.StringIO() traceback.print_exc(None, s) message = '
%s
'%cgi.escape(s.getvalue()) - self.pagehead('New %s'%self.classname.capitalize(), message) + self.pagehead(_('New %(classname)s')%{'classname': + self.classname.capitalize()}, message) newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES, self.classname) newitem.render(self.form) self.pagefoot() + def showuser(self, message=None): + '''Display a user page for editing. Make sure the user is allowed + to edit this node, and also check for password changes. + ''' + if self.user == 'anonymous': + raise Unauthorised + + user = self.db.user + + # get the username of the node being edited + node_user = user.get(self.nodeid, 'username') + + if self.user not in ('admin', node_user): + raise Unauthorised + + # + # perform any editing + # + keys = self.form.keys() + num_re = re.compile('^\d+$') + if keys: + try: + props, changed = parsePropsFromForm(self.db, user, self.form, + self.nodeid) + set_cookie = 0 + if self.nodeid == self.getuid() and changed.has_key('password'): + password = self.form['password'].value.strip() + if password: + set_cookie = password + else: + # no password was supplied - don't change it + del props['password'] + del changed['password'] + user.set(self.nodeid, **props) + # and some feedback for the user + message = _('%(changes)s edited ok')%{'changes': + ', '.join(changed.keys())} + except: + self.db.rollback() + s = StringIO.StringIO() + traceback.print_exc(None, s) + message = '
%s
'%cgi.escape(s.getvalue()) + else: + set_cookie = 0 + + # fix the cookie if the password has changed + if set_cookie: + self.set_cookie(self.user, set_cookie) + + # + # now the display + # + self.pagehead(_('User: %(user)s')%{'user': node_user}, message) + + # use the template to display the item + item = htmltemplate.ItemTemplate(self, self.TEMPLATES, 'user') + item.render(self.nodeid) + self.pagefoot() + + def showfile(self): + ''' display a file + ''' + nodeid = self.nodeid + cl = self.db.file + mime_type = cl.get(nodeid, 'type') + if mime_type == 'message/rfc822': + mime_type = 'text/plain' + self.header(headers={'Content-Type': mime_type}) + self.write(cl.get(nodeid, 'content')) + def classes(self, message=None): ''' display a list of all the classes in the database ''' if self.user == 'admin': - self.pagehead('Table of classes', message) + self.pagehead(_('Table of classes'), message) classnames = self.db.classes.keys() classnames.sort() self.write('\n') @@ -571,12 +744,15 @@ class Client: else: raise Unauthorised - def login(self, message=None): - self.pagehead('Login to roundup', message) - self.write(''' + def login(self, message=None, newuser_form=None, action='index'): + '''Display a login page. + ''' + self.pagehead(_('Login to roundup'), message) + self.write(_('''
+ @@ -584,40 +760,53 @@ class Client: -''') +''')%locals()) if self.user is None and self.ANONYMOUS_REGISTER == 'deny': self.write('
Existing User Login
Login name:
Password:
') self.pagefoot() return - self.write(''' + values = {'realname': '', 'organisation': '', 'address': '', + 'phone': '', 'username': '', 'password': '', 'confirm': '', + 'action': action} + if newuser_form is not None: + for key in newuser_form.keys(): + values[key] = newuser_form[key].value + self.write(_('''

New User Registration marked items are optional...

+ Name: - + Organisation: - + E-Mail Address: - + Phone: - + Preferred Login name: - + Password: - + Password Again: - +
-''') +''')%values) self.pagefoot() def login_action(self, message=None): + '''Attempt to log a user in and set the cookie + + returns 0 if a page is generated as a result of this call, and + 1 if not (ie. the login is successful + ''' if not self.form.has_key('__login_name'): - return self.login(message='Username required') + self.login(message=_('Username required')) + return 0 self.user = self.form['__login_name'].value if self.form.has_key('__login_password'): password = self.form['__login_password'].value @@ -629,23 +818,57 @@ class Client: except KeyError: name = self.user self.make_user_anonymous() - return self.login(message='No such user "%s"'%name) + action = self.form['__destination_url'].value + self.login(message=_('No such user "%(name)s"')%locals(), + action=action) + return 0 # and that the password is correct pw = self.db.user.get(uid, 'password') - if password != self.db.user.get(uid, 'password'): + if password != pw: self.make_user_anonymous() - return self.login(message='Incorrect password') + action = self.form['__destination_url'].value + self.login(message=_('Incorrect password'), action=action) + return 0 self.set_cookie(self.user, password) - return self.index() + return 1 + + def newuser_action(self, message=None): + '''Attempt to create a new user based on the contents of the form + and then set the cookie. + + return 1 on successful login + ''' + # re-open the database as "admin" + self.db = self.instance.open('admin') + + # TODO: pre-check the required fields and username key property + cl = self.db.user + try: + props, dummy = parsePropsFromForm(self.db, cl, self.form) + uid = cl.create(**props) + except ValueError, message: + action = self.form['__destination_url'].value + self.login(message, action=action) + return 0 + self.user = cl.get(uid, 'username') + password = cl.get(uid, 'password') + self.set_cookie(self.user, self.form['password'].value) + return 1 def set_cookie(self, user, password): # construct the cookie user = binascii.b2a_base64('%s:%s'%(user, password)).strip() + if user[-1] == '=': + if user[-2] == '=': + user = user[:-2] + else: + user = user[:-1] + expire = Cookie._getdate(86400*365) path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'])) - self.header({'Set-Cookie': 'roundup_user="%s"; Path="%s";'%(user, - path)}) + self.header({'Set-Cookie': 'roundup_user=%s; expires=%s; Path=%s;' % ( + user, expire, path)}) def make_user_anonymous(self): # make us anonymous if we can @@ -661,30 +884,14 @@ class Client: now = Cookie._getdate() path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'])) self.header({'Set-Cookie': - 'roundup_user=deleted; Max-Age=0; expires="%s"; Path="%s";'%(now, + 'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)}) - return self.login() + self.login() - def newuser_action(self, message=None): - ''' create a new user based on the contents of the form and then - set the cookie - ''' - # re-open the database as "admin" - self.db.close() - self.db = self.instance.open('admin') - - # TODO: pre-check the required fields and username key property - cl = self.db.classes['user'] - props, dummy = parsePropsFromForm(self.db, cl, self.form) - uid = cl.create(**props) - self.user = self.db.user.get(uid, 'username') - password = self.db.user.get(uid, 'password') - self.set_cookie(self.user, password) - return self.index() - - def main(self, dre=re.compile(r'([^\d]+)(\d+)'), - nre=re.compile(r'new(\w+)')): + def main(self): + '''Wrap the database accesses so we can close the database cleanly + ''' # determine the uid to use self.db = self.instance.open('admin') cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', '')) @@ -692,7 +899,14 @@ class Client: if (cookie.has_key('roundup_user') and cookie['roundup_user'].value != 'deleted'): cookie = cookie['roundup_user'].value - user, password = binascii.a2b_base64(cookie).split(':') + if len(cookie)%4: + cookie = cookie + '='*(4-len(cookie)%4) + try: + user, password = binascii.a2b_base64(cookie).split(':') + except (TypeError, binascii.Error, binascii.Incomplete): + # damaged cookie! + user, password = 'anonymous', '' + # make sure the user exists try: uid = self.db.user.lookup(user) @@ -707,13 +921,14 @@ class Client: self.make_user_anonymous() else: self.user = user - self.db.close() # re-open the database for real, using the user self.db = self.instance.open(self.user) # now figure which function to call path = self.split_path + + # default action to index if the path has no information in it if not path or path[0] in ('', 'index'): action = 'index' else: @@ -726,29 +941,65 @@ class Client: # everyone is allowed to try to log in if action == 'login_action': - return self.login_action() + # try to login + if not self.login_action(): + return + # figure the resulting page + action = self.form['__destination_url'].value + if not action: + action = 'index' + self.do_action(action) + return # allow anonymous people to register if action == 'newuser_action': # if we don't have a login and anonymous people aren't allowed to # register, then spit up the login form if self.ANONYMOUS_REGISTER == 'deny' and self.user is None: - return self.login() - return self.newuser_action() + if action == 'login': + self.login() # go to the index after login + else: + self.login(action=action) + return + # try to add the user + if not self.newuser_action(): + return + # figure the resulting page + action = self.form['__destination_url'].value + if not action: + action = 'index' + + # no login or registration, make sure totally anonymous access is OK + elif self.ANONYMOUS_ACCESS == 'deny' and self.user is None: + if action == 'login': + self.login() # go to the index after login + else: + self.login(action=action) + return - # make sure totally anonymous access is OK - if self.ANONYMOUS_ACCESS == 'deny' and self.user is None: - return self.login() + # just a regular action + self.do_action(action) + # commit all changes to the database + self.db.commit() + + def do_action(self, action, dre=re.compile(r'([^\d]+)(\d+)'), + nre=re.compile(r'new(\w+)')): + '''Figure the user's action and do it. + ''' # here be the "normal" functionality if action == 'index': - return self.index() + self.index() + return if action == 'list_classes': - return self.classes() + self.classes() + return if action == 'login': - return self.login() + self.login() + return if action == 'logout': - return self.logout() + self.logout() + return m = dre.match(action) if m: self.classname = m.group(1) @@ -765,7 +1016,8 @@ class Client: func = getattr(self, 'show%s'%self.classname) except AttributeError: raise NotFound - return func() + func() + return m = nre.match(action) if m: self.classname = m.group(1) @@ -773,7 +1025,8 @@ class Client: func = getattr(self, 'new%s'%self.classname) except AttributeError: raise NotFound - return func() + func() + return self.classname = action try: self.db.getclass(self.classname) @@ -781,9 +1034,6 @@ class Client: raise NotFound self.list() - def __del__(self): - self.db.close() - class ExtendedClient(Client): '''Includes pages and page heading information that relate to the @@ -807,61 +1057,61 @@ class ExtendedClient(Client): if port != '80': machine = machine + ':' + port base = urlparse.urlunparse(('http', machine, url, None, None, None)) if message is not None: - message = '
%s
'%message + message = _('
%(message)s
')%locals() else: message = '' style = open(os.path.join(self.TEMPLATES, 'style.css')).read() user_name = self.user or '' if self.user == 'admin': - admin_links = ' | Class List' + admin_links = _(' | Class List' \ + ' | User List' \ + ' | Add User') else: admin_links = '' if self.user not in (None, 'anonymous'): userid = self.db.user.lookup(self.user) - user_info = ''' -My Issues | -My Support | -My Details | Logout -'''%(userid, userid, userid) + user_info = _(''' +My Issues | +My Support | +My Details | Logout +''')%locals() else: - user_info = 'Login' + user_info = _('Login') if self.user is not None: - add_links = ''' + add_links = _(''' | Add Issue, Support, -User -''' +''') else: add_links = '' - self.write(''' -%s - + self.write(_(''' +%(title)s + -%s +%(message)s - - + + - +Issues, +Support +%(add_links)s +%(admin_links)s +
%s%s
%(title)s%(user_name)s
All Issues, Support | Unassigned -Issues, -Support -%s -%s%s%(user_info)s
-'''%(title, style, message, title, user_name, add_links, admin_links, - user_info)) +''')%locals()) def parsePropsFromForm(db, cl, form, nodeid=0): '''Pull properties for the given class out of the form. ''' props = {} - changed = [] + changed = {} keys = form.keys() num_re = re.compile('^\d+$') for key in keys: @@ -889,8 +1139,9 @@ def parsePropsFromForm(db, cl, form, nodeid=0): try: value = db.classes[link].lookup(value) except KeyError: - raise ValueError, 'property "%s": %s not a %s'%( - key, value, link) + raise ValueError, _('property "%(propname)s": ' + '%(value)s not a %(classname)s')%{'propname':key, + 'value': value, 'classname': link} elif isinstance(proptype, hyperdb.Multilink): value = form[key] if type(value) != type([]): @@ -900,25 +1151,263 @@ def parsePropsFromForm(db, cl, form, nodeid=0): link = cl.properties[key].classname l = [] for entry in map(str, value): + if entry == '': continue if not num_re.match(entry): try: entry = db.classes[link].lookup(entry) except KeyError: - raise ValueError, \ - 'property "%s": "%s" not an entry of %s'%(key, - entry, link.capitalize()) + raise ValueError, _('property "%(propname)s": ' + '"%(value)s" not an entry of %(classname)s')%{ + 'propname':key, 'value': entry, 'classname': link} l.append(entry) l.sort() value = l props[key] = value + + # get the old value + if nodeid: + try: + existing = cl.get(nodeid, key) + except KeyError: + # this might be a new property for which there is no existing + # value + if not cl.properties.has_key(key): raise + # if changed, set it - if nodeid and value != cl.get(nodeid, key): - changed.append(key) + if nodeid and value != existing: + changed[key] = value props[key] = value return props, changed # # $Log: not supported by cvs2svn $ +# Revision 1.89 2002/01/07 20:24:45 richard +# *mutter* stupid cutnpaste +# +# Revision 1.88 2002/01/02 02:31:38 richard +# Sorry for the huge checkin message - I was only intending to implement #496356 +# but I found a number of places where things had been broken by transactions: +# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename +# for _all_ roundup-generated smtp messages to be sent to. +# . the transaction cache had broken the roundupdb.Class set() reactors +# . newly-created author users in the mailgw weren't being committed to the db +# +# Stuff that made it into CHANGES.txt (ie. the stuff I was actually working +# on when I found that stuff :): +# . #496356 ] Use threading in messages +# . detectors were being registered multiple times +# . added tests for mailgw +# . much better attaching of erroneous messages in the mail gateway +# +# Revision 1.87 2001/12/23 23:18:49 richard +# We already had an admin-specific section of the web heading, no need to add +# another one :) +# +# Revision 1.86 2001/12/20 15:43:01 rochecompaan +# Features added: +# . Multilink properties are now displayed as comma separated values in +# a textbox +# . The add user link is now only visible to the admin user +# . Modified the mail gateway to reject submissions from unknown +# addresses if ANONYMOUS_ACCESS is denied +# +# Revision 1.85 2001/12/20 06:13:24 rochecompaan +# Bugs fixed: +# . Exception handling in hyperdb for strings-that-look-like numbers got +# lost somewhere +# . Internet Explorer submits full path for filename - we now strip away +# the path +# Features added: +# . Link and multilink properties are now displayed sorted in the cgi +# interface +# +# Revision 1.84 2001/12/18 15:30:30 rochecompaan +# Fixed bugs: +# . Fixed file creation and retrieval in same transaction in anydbm +# backend +# . Cgi interface now renders new issue after issue creation +# . Could not set issue status to resolved through cgi interface +# . Mail gateway was changing status back to 'chatting' if status was +# omitted as an argument +# +# Revision 1.83 2001/12/15 23:51:01 richard +# Tested the changes and fixed a few problems: +# . files are now attached to the issue as well as the message +# . newuser is a real method now since we don't want to do the message/file +# stuff for it +# . added some documentation +# The really big changes in the diff are a result of me moving some code +# around to keep like methods together a bit better. +# +# Revision 1.82 2001/12/15 19:24:39 rochecompaan +# . Modified cgi interface to change properties only once all changes are +# collected, files created and messages generated. +# . Moved generation of change note to nosyreactors. +# . We now check for changes to "assignedto" to ensure it's added to the +# nosy list. +# +# Revision 1.81 2001/12/12 23:55:00 richard +# Fixed some problems with user editing +# +# Revision 1.80 2001/12/12 23:27:14 richard +# Added a Zope frontend for roundup. +# +# Revision 1.79 2001/12/10 22:20:01 richard +# Enabled transaction support in the bsddb backend. It uses the anydbm code +# where possible, only replacing methods where the db is opened (it uses the +# btree opener specifically.) +# Also cleaned up some change note generation. +# Made the backends package work with pydoc too. +# +# Revision 1.78 2001/12/07 05:59:27 rochecompaan +# Fixed small bug that prevented adding issues through the web. +# +# Revision 1.77 2001/12/06 22:48:29 richard +# files multilink was being nuked in post_edit_node +# +# Revision 1.76 2001/12/05 14:26:44 rochecompaan +# Removed generation of change note from "sendmessage" in roundupdb.py. +# The change note is now generated when the message is created. +# +# Revision 1.75 2001/12/04 01:25:08 richard +# Added some rollbacks where we were catching exceptions that would otherwise +# have stopped committing. +# +# Revision 1.74 2001/12/02 05:06:16 richard +# . We now use weakrefs in the Classes to keep the database reference, so +# the close() method on the database is no longer needed. +# I bumped the minimum python requirement up to 2.1 accordingly. +# . #487480 ] roundup-server +# . #487476 ] INSTALL.txt +# +# I also cleaned up the change message / post-edit stuff in the cgi client. +# There's now a clearly marked "TODO: append the change note" where I believe +# the change note should be added there. The "changes" list will obviously +# have to be modified to be a dict of the changes, or somesuch. +# +# More testing needed. +# +# Revision 1.73 2001/12/01 07:17:50 richard +# . We now have basic transaction support! Information is only written to +# the database when the commit() method is called. Only the anydbm +# backend is modified in this way - neither of the bsddb backends have been. +# The mail, admin and cgi interfaces all use commit (except the admin tool +# doesn't have a commit command, so interactive users can't commit...) +# . Fixed login/registration forwarding the user to the right page (or not, +# on a failure) +# +# Revision 1.72 2001/11/30 20:47:58 rochecompaan +# Links in page header are now consistent with default sort order. +# +# Fixed bugs: +# - When login failed the list of issues were still rendered. +# - User was redirected to index page and not to his destination url +# if his first login attempt failed. +# +# Revision 1.71 2001/11/30 20:28:10 rochecompaan +# Property changes are now completely traceable, whether changes are +# made through the web or by email +# +# Revision 1.70 2001/11/30 00:06:29 richard +# Converted roundup/cgi_client.py to use _() +# Added the status file, I18N_PROGRESS.txt +# +# Revision 1.69 2001/11/29 23:19:51 richard +# Removed the "This issue has been edited through the web" when a valid +# change note is supplied. +# +# Revision 1.68 2001/11/29 04:57:23 richard +# a little comment +# +# Revision 1.67 2001/11/28 21:55:35 richard +# . login_action and newuser_action return values were being ignored +# . Woohoo! Found that bloody re-login bug that was killing the mail +# gateway. +# (also a minor cleanup in hyperdb) +# +# Revision 1.66 2001/11/27 03:00:50 richard +# couple of bugfixes from latest patch integration +# +# Revision 1.65 2001/11/26 23:00:53 richard +# This config stuff is getting to be a real mess... +# +# Revision 1.64 2001/11/26 22:56:35 richard +# typo +# +# Revision 1.63 2001/11/26 22:55:56 richard +# Feature: +# . Added INSTANCE_NAME to configuration - used in web and email to identify +# the instance. +# . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup +# signature info in e-mails. +# . Some more flexibility in the mail gateway and more error handling. +# . Login now takes you to the page you back to the were denied access to. +# +# Fixed: +# . Lots of bugs, thanks Roché and others on the devel mailing list! +# +# Revision 1.62 2001/11/24 00:45:42 jhermann +# typeof() instead of type(): avoid clash with database field(?) "type" +# +# Fixes this traceback: +# +# Traceback (most recent call last): +# File "roundup\cgi_client.py", line 535, in newnode +# self._post_editnode(nid) +# File "roundup\cgi_client.py", line 415, in _post_editnode +# if type(value) != type([]): value = [value] +# UnboundLocalError: local variable 'type' referenced before assignment +# +# Revision 1.61 2001/11/22 15:46:42 jhermann +# Added module docstrings to all modules. +# +# Revision 1.60 2001/11/21 22:57:28 jhermann +# Added dummy hooks for I18N and some preliminary (test) markup of +# translatable messages +# +# Revision 1.59 2001/11/21 03:21:13 richard +# oops +# +# Revision 1.58 2001/11/21 03:11:28 richard +# Better handling of new properties. +# +# Revision 1.57 2001/11/15 10:24:27 richard +# handle the case where there is no file attached +# +# Revision 1.56 2001/11/14 21:35:21 richard +# . users may attach files to issues (and support in ext) through the web now +# +# Revision 1.55 2001/11/07 02:34:06 jhermann +# Handling of damaged login cookies +# +# Revision 1.54 2001/11/07 01:16:12 richard +# Remove the '=' padding from cookie value so quoting isn't an issue. +# +# Revision 1.53 2001/11/06 23:22:05 jhermann +# More IE fixes: it does not like quotes around cookie values; in the +# hope this does not break anything for other browser; if it does, we +# need to check HTTP_USER_AGENT +# +# Revision 1.52 2001/11/06 23:11:22 jhermann +# Fixed debug output in page footer; added expiry date to the login cookie +# (expires 1 year in the future) to prevent probs with certain versions +# of IE +# +# Revision 1.51 2001/11/06 22:00:34 jhermann +# Get debug level from ROUNDUP_DEBUG env var +# +# Revision 1.50 2001/11/05 23:45:40 richard +# Fixed newuser_action so it sets the cookie with the unencrypted password. +# Also made it present nicer error messages (not tracebacks). +# +# Revision 1.49 2001/11/04 03:07:12 richard +# Fixed various cookie-related bugs: +# . bug #477685 ] base64.decodestring breaks +# . bug #477837 ] lynx does not like the cookie +# . bug #477892 ] Password edit doesn't fix login cookie +# Also closed a security hole - a logged-in user could edit another user's +# details. +# # Revision 1.48 2001/11/03 01:30:18 richard # Oops. uses pagefoot now. #