summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: 1fa6242)
raw | patch | inline | side by side (parent: 1fa6242)
author | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Fri, 30 Aug 2002 08:28:44 +0000 (08:28 +0000) | ||
committer | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Fri, 30 Aug 2002 08:28:44 +0000 (08:28 +0000) |
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1002 57a73879-2fb5-44c3-a270-3262357dd7e2
roundup/cgi/Acquisition.py | [new file with mode: 0644] | patch | blob |
roundup/cgi/ComputedAttribute.py | [new file with mode: 0644] | patch | blob |
roundup/cgi/ExtensionClass.py | [new file with mode: 0644] | patch | blob |
roundup/cgi/MultiMapping.py | [new file with mode: 0644] | patch | blob |
roundup/cgi/__init__.py | [new file with mode: 0644] | patch | blob |
roundup/cgi/cgitb.py | [new file with mode: 0644] | patch | blob |
roundup/cgi/client.py | [new file with mode: 0644] | patch | blob |
roundup/cgi/templating.py | [new file with mode: 0644] | patch | blob |
roundup/cgi/zLOG.py | [new file with mode: 0644] | patch | blob |
diff --git a/roundup/cgi/Acquisition.py b/roundup/cgi/Acquisition.py
--- /dev/null
@@ -0,0 +1,8 @@
+class Explicit:
+ pass
+
+def aq_base(obj):
+ return obj
+aq_inner = aq_base
+def aq_parent(obj):
+ return None
diff --git a/roundup/cgi/ComputedAttribute.py b/roundup/cgi/ComputedAttribute.py
--- /dev/null
@@ -0,0 +1,11 @@
+class ComputedAttribute:
+ def __init__(self, callable, level):
+ self.callable = callable
+ self.level = level
+ def __of__(self, *args):
+ if self.level > 0:
+ return self.callable
+ if isinstance(self.callable, type('')):
+ return getattr(args[0], self.callable)
+ return self.callable(*args)
+
diff --git a/roundup/cgi/ExtensionClass.py b/roundup/cgi/ExtensionClass.py
--- /dev/null
@@ -0,0 +1,2 @@
+class Base:
+ pass
diff --git a/roundup/cgi/MultiMapping.py b/roundup/cgi/MultiMapping.py
--- /dev/null
@@ -0,0 +1,25 @@
+import operator
+
+class MultiMapping:
+ def __init__(self, *stores):
+ self.stores = list(stores)
+ def __getitem__(self, key):
+ for store in self.stores:
+ if store.has_key(key):
+ return store[key]
+ raise KeyError, key
+ _marker = []
+ def get(self, key, default=_marker):
+ for store in self.stores:
+ if store.has_key(key):
+ return store[key]
+ if default is self._marker:
+ raise KeyError, key
+ return default
+ def __len__(self):
+ return reduce(operator.add, [len(x) for x in stores], 0)
+ def push(self, store):
+ self.stores.append(store)
+ def pop(self):
+ return self.stores.pop()
+
diff --git a/roundup/cgi/__init__.py b/roundup/cgi/__init__.py
diff --git a/roundup/cgi/cgitb.py b/roundup/cgi/cgitb.py
--- /dev/null
+++ b/roundup/cgi/cgitb.py
@@ -0,0 +1,161 @@
+#
+# This module was written by Ka-Ping Yee, <ping@lfw.org>.
+#
+# $Id: cgitb.py,v 1.1 2002-08-30 08:28:44 richard Exp $
+
+__doc__ = """
+Extended CGI traceback handler by Ka-Ping Yee, <ping@lfw.org>.
+"""
+
+import sys, os, types, string, keyword, linecache, tokenize, inspect, pydoc
+
+from roundup.i18n import _
+
+def breaker():
+ return ('<body bgcolor="white">' +
+ '<font color="white" size="-5"> > </font> ' +
+ '</table>' * 5)
+
+def html(context=5):
+ etype, evalue = sys.exc_type, sys.exc_value
+ if type(etype) is types.ClassType:
+ etype = etype.__name__
+ pyver = 'Python ' + string.split(sys.version)[0] + '<br>' + sys.executable
+ head = pydoc.html.heading(
+ '<font size=+1><strong>%s</strong>: %s</font>'%(etype, evalue),
+ '#ffffff', '#777777', pyver)
+
+ head = head + (_('<p>A problem occurred while running a Python script. '
+ 'Here is the sequence of function calls leading up to '
+ 'the error, with the most recent (innermost) call first. '
+ 'The exception attributes are:'))
+
+ indent = '<tt><small>%s</small> </tt>' % (' ' * 5)
+ traceback = []
+ for frame, file, lnum, func, lines, index in inspect.trace(context):
+ if file is None:
+ link = '<file is None - probably inside <tt>eval</tt> or <tt>exec</tt>>'
+ else:
+ file = os.path.abspath(file)
+ link = '<a href="file:%s">%s</a>' % (file, pydoc.html.escape(file))
+ args, varargs, varkw, locals = inspect.getargvalues(frame)
+ if func == '?':
+ call = ''
+ else:
+ call = 'in <strong>%s</strong>' % func + inspect.formatargvalues(
+ args, varargs, varkw, locals,
+ formatvalue=lambda value: '=' + pydoc.html.repr(value))
+
+ level = '''
+<table width="100%%" bgcolor="#dddddd" cellspacing=0 cellpadding=2 border=0>
+<tr><td>%s %s</td></tr></table>''' % (link, call)
+
+ if index is None or file is None:
+ traceback.append('<p>' + level)
+ continue
+
+ # do a fil inspection
+ names = []
+ def tokeneater(type, token, start, end, line, names=names):
+ if type == tokenize.NAME and token not in keyword.kwlist:
+ if token not in names:
+ names.append(token)
+ if type == tokenize.NEWLINE: raise IndexError
+ def linereader(file=file, lnum=[lnum]):
+ line = linecache.getline(file, lnum[0])
+ lnum[0] = lnum[0] + 1
+ return line
+
+ try:
+ tokenize.tokenize(linereader, tokeneater)
+ except IndexError: pass
+ lvals = []
+ for name in names:
+ if name in frame.f_code.co_varnames:
+ if locals.has_key(name):
+ value = pydoc.html.repr(locals[name])
+ else:
+ value = _('<em>undefined</em>')
+ name = '<strong>%s</strong>' % name
+ else:
+ if frame.f_globals.has_key(name):
+ value = pydoc.html.repr(frame.f_globals[name])
+ else:
+ value = _('<em>undefined</em>')
+ name = '<em>global</em> <strong>%s</strong>' % name
+ lvals.append('%s = %s' % (name, value))
+ if lvals:
+ lvals = string.join(lvals, ', ')
+ lvals = indent + '''
+<small><font color="#909090">%s</font></small><br>''' % lvals
+ else:
+ lvals = ''
+
+ excerpt = []
+ i = lnum - index
+ for line in lines:
+ number = ' ' * (5-len(str(i))) + str(i)
+ number = '<small><font color="#909090">%s</font></small>' % number
+ line = '<tt>%s %s</tt>' % (number, pydoc.html.preformat(line))
+ if i == lnum:
+ line = '''
+<table width="100%%" bgcolor="#white" cellspacing=0 cellpadding=0 border=0>
+<tr><td>%s</td></tr></table>''' % line
+ excerpt.append('\n' + line)
+ if i == lnum:
+ excerpt.append(lvals)
+ i = i + 1
+ traceback.append('<p>' + level + string.join(excerpt, '\n'))
+
+ traceback.reverse()
+
+ exception = '<p><strong>%s</strong>: %s' % (str(etype), str(evalue))
+ attribs = []
+ if type(evalue) is types.InstanceType:
+ for name in dir(evalue):
+ value = pydoc.html.repr(getattr(evalue, name))
+ attribs.append('<br>%s%s = %s' % (indent, name, value))
+
+ return head + string.join(attribs) + string.join(traceback) + '<p> </p>'
+
+def handler():
+ print breaker()
+ print html()
+
+#
+# $Log: not supported by cvs2svn $
+# Revision 1.10 2002/01/16 04:49:45 richard
+# Handle a special case that the CGI interface tickles. I need to check if
+# this needs fixing in python's core.
+#
+# Revision 1.9 2002/01/08 11:56:24 richard
+# missed an import _
+#
+# Revision 1.8 2002/01/05 02:22:32 richard
+# i18n'ification
+#
+# Revision 1.7 2001/11/22 15:46:42 jhermann
+# Added module docstrings to all modules.
+#
+# Revision 1.6 2001/09/29 13:27:00 richard
+# CGI interfaces now spit up a top-level index of all the instances they can
+# serve.
+#
+# Revision 1.5 2001/08/07 00:24:42 richard
+# stupid typo
+#
+# Revision 1.4 2001/08/07 00:15:51 richard
+# Added the copyright/license notice to (nearly) all files at request of
+# Bizar Software.
+#
+# Revision 1.3 2001/07/29 07:01:39 richard
+# Added vim command to all source so that we don't get no steenkin' tabs :)
+#
+# Revision 1.2 2001/07/22 12:09:32 richard
+# Final commit of Grande Splite
+#
+# Revision 1.1 2001/07/22 11:58:35 richard
+# More Grande Splite
+#
+#
+# vim: set filetype=python ts=4 sw=4 et si
diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
--- /dev/null
+++ b/roundup/cgi/client.py
@@ -0,0 +1,915 @@
+# $Id: client.py,v 1.1 2002-08-30 08:28:44 richard Exp $
+
+__doc__ = """
+WWW request handler (also used in the stand-alone server).
+"""
+
+import os, cgi, StringIO, urlparse, re, traceback, mimetypes, urllib
+import binascii, Cookie, time, random
+
+from roundup import roundupdb, date, hyperdb, password
+from roundup.i18n import _
+
+from roundup.cgi.templating import RoundupPageTemplate
+from roundup.cgi import cgitb
+from PageTemplates import PageTemplate
+
+class Unauthorised(ValueError):
+ pass
+
+class NotFound(ValueError):
+ pass
+
+class Redirect(Exception):
+ pass
+
+class SendFile(Exception):
+ ' Sent a file from the database '
+
+class SendStaticFile(Exception):
+ ' Send a static file from the instance html directory '
+
+def initialiseSecurity(security):
+ ''' Create some Permissions and Roles on the security object
+
+ This function is directly invoked by security.Security.__init__()
+ as a part of the Security object instantiation.
+ '''
+ security.addPermission(name="Web Registration",
+ description="User may register through the web")
+ p = security.addPermission(name="Web Access",
+ description="User may access the web interface")
+ security.addPermissionToRole('Admin', p)
+
+ # doing Role stuff through the web - make sure Admin can
+ p = security.addPermission(name="Web Roles",
+ description="User may manipulate user Roles through the web")
+ security.addPermissionToRole('Admin', p)
+
+class Client:
+ '''
+ A note about login
+ ------------------
+
+ If the user has no login cookie, then they are anonymous. There
+ are two levels of anonymous use. If there is no 'anonymous' user, there
+ is no login at all and the database is opened in read-only mode. If the
+ 'anonymous' user exists, the user is logged in using that user (though
+ there is no cookie). This allows them to modify the database, and all
+ modifications are attributed to the 'anonymous' user.
+
+ Once a user logs in, they are assigned a session. The Client instance
+ keeps the nodeid of the session as the "session" attribute.
+ '''
+
+ def __init__(self, instance, request, env, form=None):
+ hyperdb.traceMark()
+ self.instance = instance
+ self.request = request
+ self.env = env
+ self.path = env['PATH_INFO']
+ self.split_path = self.path.split('/')
+ self.instance_path_name = env['INSTANCE_NAME']
+ url = self.env['SCRIPT_NAME'] + '/' + self.instance_path_name
+ machine = self.env['SERVER_NAME']
+ port = self.env['SERVER_PORT']
+ if port != '80': machine = machine + ':' + port
+ self.base = urlparse.urlunparse(('http', env['HTTP_HOST'], url,
+ None, None, None))
+
+ if form is None:
+ self.form = cgi.FieldStorage(environ=env)
+ else:
+ self.form = form
+ self.headers_done = 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 main(self):
+ ''' Wrap the request and handle unauthorised requests
+ '''
+ self.content_action = None
+ self.ok_message = []
+ self.error_message = []
+ try:
+ # make sure we're identified (even anonymously)
+ self.determine_user()
+ # figure out the context and desired content template
+ self.determine_context()
+ # possibly handle a form submit action (may change self.message
+ # and self.template_name)
+ self.handle_action()
+ # now render the page
+ self.write(self.template('page', ok_message=self.ok_message,
+ error_message=self.error_message))
+ except Redirect, url:
+ # let's redirect - if the url isn't None, then we need to do
+ # the headers, otherwise the headers have been set before the
+ # exception was raised
+ if url:
+ self.header({'Location': url}, response=302)
+ except SendFile, designator:
+ self.serve_file(designator)
+ except SendStaticFile, file:
+ self.serve_static_file(file)
+ except Unauthorised, message:
+ self.write(self.template('page.unauthorised',
+ error_message=message))
+ except:
+ # everything else
+ self.write(cgitb.html())
+
+ def determine_user(self):
+ ''' Determine who the user is
+ '''
+ # determine the uid to use
+ self.opendb('admin')
+
+ # make sure we have the session Class
+ sessions = self.db.sessions
+
+ # age sessions, remove when they haven't been used for a week
+ # TODO: this shouldn't be done every access
+ week = 60*60*24*7
+ now = time.time()
+ for sessid in sessions.list():
+ interval = now - sessions.get(sessid, 'last_use')
+ if interval > week:
+ sessions.destroy(sessid)
+
+ # look up the user session cookie
+ cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
+ user = 'anonymous'
+
+ if (cookie.has_key('roundup_user') and
+ cookie['roundup_user'].value != 'deleted'):
+
+ # get the session key from the cookie
+ self.session = cookie['roundup_user'].value
+ # get the user from the session
+ try:
+ # update the lifetime datestamp
+ sessions.set(self.session, last_use=time.time())
+ sessions.commit()
+ user = sessions.get(self.session, 'user')
+ except KeyError:
+ user = 'anonymous'
+
+ # sanity check on the user still being valid, getting the userid
+ # at the same time
+ try:
+ self.userid = self.db.user.lookup(user)
+ except (KeyError, TypeError):
+ user = 'anonymous'
+
+ # make sure the anonymous user is valid if we're using it
+ if user == 'anonymous':
+ self.make_user_anonymous()
+ else:
+ self.user = user
+
+ def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
+ ''' Determine the context of this page:
+
+ home (default if no url is given)
+ classname
+ designator (classname and nodeid)
+
+ The desired template to be rendered is also determined There
+ are two exceptional contexts:
+
+ _file - serve up a static file
+ path len > 1 - serve up a FileClass content
+ (the additional path gives the browser a
+ nicer filename to save as)
+
+ The template used is specified by the :template CGI variable,
+ which defaults to:
+ only classname suplied: "index"
+ full item designator supplied: "item"
+
+ We set:
+ self.classname
+ self.nodeid
+ self.template_name
+ '''
+ # default the optional variables
+ self.classname = None
+ self.nodeid = None
+
+ # determine the classname and possibly nodeid
+ path = self.split_path
+ if not path or path[0] in ('', 'home', 'index'):
+ if self.form.has_key(':template'):
+ self.template_type = self.form[':template'].value
+ self.template_name = 'home' + '.' + self.template_type
+ else:
+ self.template_type = ''
+ self.template_name = 'home'
+ return
+ elif path[0] == '_file':
+ raise SendStaticFile, path[1]
+ else:
+ self.classname = path[0]
+ if len(path) > 1:
+ # send the file identified by the designator in path[0]
+ raise SendFile, path[0]
+
+ # see if we got a designator
+ m = dre.match(self.classname)
+ if m:
+ self.classname = m.group(1)
+ self.nodeid = m.group(2)
+ # with a designator, we default to item view
+ self.template_type = 'item'
+ else:
+ # with only a class, we default to index view
+ self.template_type = 'index'
+
+ # see if we have a template override
+ if self.form.has_key(':template'):
+ self.template_type = self.form[':template'].value
+
+
+ # see if we were passed in a message
+ if self.form.has_key(':ok_message'):
+ self.ok_message.append(self.form[':ok_message'].value)
+ if self.form.has_key(':error_message'):
+ self.error_message.append(self.form[':error_message'].value)
+
+ # we have the template name now
+ self.template_name = self.classname + '.' + self.template_type
+
+ def serve_file(self, designator, dre=re.compile(r'([^\d]+)(\d+)')):
+ ''' Serve the file from the content property of the designated item.
+ '''
+ m = dre.match(str(designator))
+ if not m:
+ raise NotFound, str(designator)
+ classname, nodeid = m.group(1), m.group(2)
+ if classname != 'file':
+ raise NotFound, designator
+
+ # we just want to serve up the file named
+ file = self.db.file
+ self.header({'Content-Type': file.get(nodeid, 'type')})
+ self.write(file.get(nodeid, 'content'))
+
+ def serve_static_file(self, file):
+ # we just want to serve up the file named
+ mt = mimetypes.guess_type(str(file))[0]
+ self.header({'Content-Type': mt})
+ self.write(open('/tmp/test/html/%s'%file).read())
+
+ def template(self, name, **kwargs):
+ ''' Return a PageTemplate for the named page
+ '''
+ pt = RoundupPageTemplate(self)
+ # make errors nicer
+ pt.id = name
+ pt.write(open('/tmp/test/html/%s'%name).read())
+ # XXX handle PT rendering errors here nicely
+ try:
+ return pt.render(**kwargs)
+ except PageTemplate.PTRuntimeError, message:
+ return '<strong>%s</strong><ol>%s</ol>'%(message,
+ cgi.escape('<li>'.join(pt._v_errors)))
+ except:
+ # everything else
+ return cgitb.html()
+
+ def content(self):
+ ''' Callback used by the page template to render the content of
+ the page.
+ '''
+ # now render the page content using the template we determined in
+ # determine_context
+ return self.template(self.template_name)
+
+ # these are the actions that are available
+ actions = {
+ 'edit': 'edititem_action',
+ 'new': 'newitem_action',
+ 'login': 'login_action',
+ 'logout': 'logout_action',
+ 'register': 'register_action',
+ }
+ def handle_action(self):
+ ''' Determine whether there should be an _action called.
+
+ The action is defined by the form variable :action which
+ identifies the method on this object to call. The four basic
+ actions are defined in the "actions" dictionary on this class:
+ "edit" -> self.edititem_action
+ "new" -> self.newitem_action
+ "login" -> self.login_action
+ "logout" -> self.logout_action
+ "register" -> self.register_action
+
+ '''
+ if not self.form.has_key(':action'):
+ return None
+ try:
+ # get the action, validate it
+ action = self.form[':action'].value
+ if not self.actions.has_key(action):
+ raise ValueError, 'No such action "%s"'%action
+
+ # call the mapped action
+ getattr(self, self.actions[action])()
+ except Redirect:
+ raise
+ except:
+ self.db.rollback()
+ s = StringIO.StringIO()
+ traceback.print_exc(None, s)
+ self.error_message.append('<pre>%s</pre>'%cgi.escape(s.getvalue()))
+
+ def write(self, content):
+ if not self.headers_done:
+ self.header()
+ self.request.wfile.write(content)
+
+ def header(self, headers=None, response=200):
+ '''Put up the appropriate header.
+ '''
+ if headers is None:
+ headers = {'Content-Type':'text/html'}
+ if not headers.has_key('Content-Type'):
+ headers['Content-Type'] = 'text/html'
+ self.request.send_response(response)
+ for entry in headers.items():
+ self.request.send_header(*entry)
+ self.request.end_headers()
+ self.headers_done = 1
+ if self.debug:
+ self.headers_sent = headers
+
+ def set_cookie(self, user, password):
+ # TODO generate a much, much stronger session key ;)
+ self.session = binascii.b2a_base64(repr(time.time())).strip()
+
+ # clean up the base64
+ if self.session[-1] == '=':
+ if self.session[-2] == '=':
+ self.session = self.session[:-2]
+ else:
+ self.session = self.session[:-1]
+
+ # insert the session in the sessiondb
+ self.db.sessions.set(self.session, user=user, last_use=time.time())
+
+ # and commit immediately
+ self.db.sessions.commit()
+
+ # expire us in a long, long time
+ expire = Cookie._getdate(86400*365)
+
+ # generate the cookie path - make sure it has a trailing '/'
+ path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
+ ''))
+ self.header({'Set-Cookie': 'roundup_user=%s; expires=%s; Path=%s;'%(
+ self.session, expire, path)})
+
+ def make_user_anonymous(self):
+ ''' Make us anonymous
+
+ This method used to handle non-existence of the 'anonymous'
+ user, but that user is mandatory now.
+ '''
+ self.userid = self.db.user.lookup('anonymous')
+ self.user = 'anonymous'
+
+ def logout(self):
+ ''' Make us really anonymous - nuke the cookie too
+ '''
+ self.make_user_anonymous()
+
+ # construct the logout cookie
+ 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,
+ path)})
+ self.login()
+
+ def opendb(self, user):
+ ''' Open the database.
+ '''
+ # open the db if the user has changed
+ if not hasattr(self, 'db') or user != self.db.journaltag:
+ self.db = self.instance.open(user)
+
+ #
+ # Actions
+ #
+ def login_action(self):
+ ''' Attempt to log a user in and set the cookie
+ '''
+ # we need the username at a minimum
+ if not self.form.has_key('__login_name'):
+ self.error_message.append(_('Username required'))
+ return
+
+ self.user = self.form['__login_name'].value
+ # re-open the database for real, using the user
+ self.opendb(self.user)
+ if self.form.has_key('__login_password'):
+ password = self.form['__login_password'].value
+ else:
+ password = ''
+ # make sure the user exists
+ try:
+ self.userid = self.db.user.lookup(self.user)
+ except KeyError:
+ name = self.user
+ self.make_user_anonymous()
+ self.error_message.append(_('No such user "%(name)s"')%locals())
+ return
+
+ # and that the password is correct
+ pw = self.db.user.get(self.userid, 'password')
+ if password != pw:
+ self.make_user_anonymous()
+ self.error_message.append(_('Incorrect password'))
+ return
+
+ # set the session cookie
+ self.set_cookie(self.user, password)
+
+ def logout_action(self):
+ ''' Make us really anonymous - nuke the cookie too
+ '''
+ # log us out
+ self.make_user_anonymous()
+
+ # construct the logout cookie
+ now = Cookie._getdate()
+ path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
+ ''))
+ self.header(headers={'Set-Cookie':
+ 'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
+# 'Location': self.db.config.DEFAULT_VIEW}, response=301)
+
+ # suboptimal, but will do for now
+ self.ok_message.append(_('You are logged out'))
+ #raise Redirect, None
+
+ def register_action(self):
+ '''Attempt to create a new user based on the contents of the form
+ and then set the cookie.
+
+ return 1 on successful login
+ '''
+ # make sure we're allowed to register
+ userid = self.db.user.lookup(self.user)
+ if not self.db.security.hasPermission('Web Registration', userid):
+ raise Unauthorised, _("You do not have permission to access"\
+ " %(action)s.")%{'action': 'registration'}
+
+ # re-open the database as "admin"
+ if self.user != 'admin':
+ self.opendb('admin')
+
+ # create the new user
+ cl = self.db.user
+ try:
+ props = parsePropsFromForm(self.db, cl, self.form)
+ props['roles'] = self.instance.NEW_WEB_USER_ROLES
+ uid = cl.create(**props)
+ self.db.commit()
+ except ValueError, message:
+ self.error_message.append(message)
+
+ # log the new user in
+ self.user = cl.get(uid, 'username')
+ # re-open the database for real, using the user
+ self.opendb(self.user)
+ password = cl.get(uid, 'password')
+ self.set_cookie(self.user, password)
+
+ # nice message
+ self.ok_message.append(_('You are now registered, welcome!'))
+
+ def edititem_action(self):
+ ''' Perform an edit of an item in the database.
+
+ Some special form elements:
+
+ :link=designator:property
+ :multilink=designator:property
+ The value specifies a node designator and the property on that
+ node to add _this_ node to as a link or multilink.
+ __note
+ Create a message and attach it to the current node's
+ "messages" property.
+ __file
+ Create a file and attach it to the current node's
+ "files" property. Attach the file to the message created from
+ the __note if it's supplied.
+ '''
+ cn = self.classname
+ cl = self.db.classes[cn]
+
+ # check permission
+ userid = self.db.user.lookup(self.user)
+ if not self.db.security.hasPermission('Edit', userid, cn):
+ self.error_message.append(
+ _('You do not have permission to edit %s' %cn))
+
+ # perform the edit
+ props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
+
+ # make changes to the node
+ props = self._changenode(props)
+
+ # handle linked nodes
+ self._post_editnode(self.nodeid)
+
+ # commit now that all the tricky stuff is done
+ self.db.commit()
+
+ # and some nice feedback for the user
+ if props:
+ message = _('%(changes)s edited ok')%{'changes':
+ ', '.join(props.keys())}
+ elif self.form.has_key('__note') and self.form['__note'].value:
+ message = _('note added')
+ elif (self.form.has_key('__file') and self.form['__file'].filename):
+ message = _('file added')
+ else:
+ message = _('nothing changed')
+
+ # redirect to the item's edit page
+ raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, cn, self.nodeid,
+ urllib.quote(message))
+
+ def newitem_action(self):
+ ''' Add a new item to the database.
+
+ This follows the same form as the edititem_action
+ '''
+ # check permission
+ cn = self.classname
+ userid = self.db.user.lookup(self.user)
+ if not self.db.security.hasPermission('Edit', userid, cn):
+ self.error_message.append(
+ _('You do not have permission to create %s' %cn))
+
+ # XXX
+# cl = self.db.classes[cn]
+# if self.form.has_key(':multilink'):
+# link = self.form[':multilink'].value
+# designator, linkprop = link.split(':')
+# xtra = ' for <a href="%s">%s</a>' % (designator, designator)
+# else:
+# xtra = ''
+
+ try:
+ # do the create
+ nid = self._createnode()
+
+ # handle linked nodes
+ self._post_editnode(nid)
+
+ # commit now that all the tricky stuff is done
+ self.db.commit()
+
+ # render the newly created item
+ self.nodeid = nid
+
+ # and some nice feedback for the user
+ message = _('%(classname)s created ok')%{'classname': cn}
+ except:
+ # oops
+ self.db.rollback()
+ s = StringIO.StringIO()
+ traceback.print_exc(None, s)
+ self.error_message.append('<pre>%s</pre>'%cgi.escape(s.getvalue()))
+
+ # redirect to the new item's page
+ raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, cn, nid,
+ urllib.quote(message))
+
+ def genericedit_action(self):
+ ''' Performs an edit of all of a class' items in one go.
+
+ The "rows" CGI var defines the CSV-formatted entries for the
+ class. New nodes are identified by the ID 'X' (or any other
+ non-existent ID) and removed lines are retired.
+ '''
+ userid = self.db.user.lookup(self.user)
+ if not self.db.security.hasPermission('Edit', userid):
+ raise Unauthorised, _("You do not have permission to access"\
+ " %(action)s.")%{'action': self.classname}
+ w = self.write
+ cn = self.classname
+ cl = self.db.classes[cn]
+ idlessprops = cl.getprops(protected=0).keys()
+ props = ['id'] + idlessprops
+
+ # get the CSV module
+ try:
+ import csv
+ except ImportError:
+ self.error_message.append(_(
+ 'Sorry, you need the csv module to use this function.<br>\n'
+ 'Get it from: <a href="http://www.object-craft.com.au/projects/csv/">http://www.object-craft.com.au/projects/csv/'))
+ return
+
+ # do the edit
+ rows = self.form['rows'].value.splitlines()
+ p = csv.parser()
+ found = {}
+ line = 0
+ for row in rows:
+ line += 1
+ values = p.parse(row)
+ # not a complete row, keep going
+ if not values: continue
+
+ # extract the nodeid
+ nodeid, values = values[0], values[1:]
+ found[nodeid] = 1
+
+ # confirm correct weight
+ if len(idlessprops) != len(values):
+ w(_('Not enough values on line %(line)s'%{'line':line}))
+ return
+
+ # extract the new values
+ d = {}
+ for name, value in zip(idlessprops, values):
+ value = value.strip()
+ # only add the property if it has a value
+ if value:
+ # if it's a multilink, split it
+ if isinstance(cl.properties[name], hyperdb.Multilink):
+ value = value.split(':')
+ d[name] = value
+
+ # perform the edit
+ if cl.hasnode(nodeid):
+ # edit existing
+ cl.set(nodeid, **d)
+ else:
+ # new node
+ found[cl.create(**d)] = 1
+
+ # retire the removed entries
+ for nodeid in cl.list():
+ if not found.has_key(nodeid):
+ cl.retire(nodeid)
+
+ message = _('items edited OK')
+
+ # redirect to the class' edit page
+ raise Redirect, '%s/%s?:ok_message=%s'%(self.base, cn,
+ urllib.quote(message))
+
+ def _changenode(self, props):
+ ''' change the node based on the contents of the form
+ '''
+ cl = self.db.classes[self.classname]
+
+ # 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
+ return 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 = parsePropsFromForm(self.db, cl, self.form)
+
+ # 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 _handle_message(self):
+ ''' generate an 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.strip()
+ if not note:
+ return None, files
+ 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 '\n' in note:
+ summary = re.split(r'\n\r?', note)[0]
+ else:
+ summary = note
+ m = ['%s\n'%note]
+
+ # handle the messageid
+ # TODO: handle inreplyto
+ messageid = "<%s.%s.%s@%s>"%(time.time(), random.random(),
+ self.classname, self.instance.MAIL_DOMAIN)
+
+ # now create the message, attaching the files
+ content = '\n'.join(m)
+ message_id = self.db.msg.create(author=self.userid,
+ 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]
+ # link if necessary
+ keys = self.form.keys()
+ for key in keys:
+ if key == ':multilink':
+ value = self.form[key].value
+ if type(value) != type([]): value = [value]
+ for value in value:
+ designator, property = value.split(':')
+ link, nodeid = hyperdb.splitDesignator(designator)
+ link = self.db.classes[link]
+ # take a dupe of the list so we're not changing the cache
+ value = link.get(nodeid, property)[:]
+ value.append(nid)
+ link.set(nodeid, **{property: value})
+ elif key == ':link':
+ value = self.form[key].value
+ if type(value) != type([]): value = [value]
+ for value in value:
+ designator, property = value.split(':')
+ link, nodeid = hyperdb.splitDesignator(designator)
+ link = self.db.classes[link]
+ link.set(nodeid, **{property: nid})
+
+
+ def remove_action(self, dre=re.compile(r'([^\d]+)(\d+)')):
+ # XXX handle this !
+ target = self.index_arg(':target')[0]
+ m = dre.match(target)
+ if m:
+ classname = m.group(1)
+ nodeid = m.group(2)
+ cl = self.db.getclass(classname)
+ cl.retire(nodeid)
+ # now take care of the reference
+ parentref = self.index_arg(':multilink')[0]
+ parent, prop = parentref.split(':')
+ m = dre.match(parent)
+ if m:
+ self.classname = m.group(1)
+ self.nodeid = m.group(2)
+ cl = self.db.getclass(self.classname)
+ value = cl.get(self.nodeid, prop)
+ value.remove(nodeid)
+ cl.set(self.nodeid, **{prop:value})
+ func = getattr(self, 'show%s'%self.classname)
+ return func()
+ else:
+ raise NotFound, parent
+ else:
+ raise NotFound, target
+
+
+def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
+ '''Pull properties for the given class out of the form.
+ '''
+ props = {}
+ keys = form.keys()
+ for key in keys:
+ if not cl.properties.has_key(key):
+ continue
+ proptype = cl.properties[key]
+ if isinstance(proptype, hyperdb.String):
+ value = form[key].value.strip()
+ elif isinstance(proptype, hyperdb.Password):
+ value = password.Password(form[key].value.strip())
+ elif isinstance(proptype, hyperdb.Date):
+ value = form[key].value.strip()
+ if value:
+ value = date.Date(form[key].value.strip())
+ else:
+ value = None
+ elif isinstance(proptype, hyperdb.Interval):
+ value = form[key].value.strip()
+ if value:
+ value = date.Interval(form[key].value.strip())
+ else:
+ value = None
+ elif isinstance(proptype, hyperdb.Link):
+ value = form[key].value.strip()
+ # see if it's the "no selection" choice
+ if value == '-1':
+ value = None
+ else:
+ # handle key values
+ link = cl.properties[key].classname
+ if not num_re.match(value):
+ try:
+ value = db.classes[link].lookup(value)
+ except KeyError:
+ 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 hasattr(value, 'value'):
+ # Quite likely to be a FormItem instance
+ value = value.value
+ if not isinstance(value, type([])):
+ value = [i.strip() for i in value.split(',')]
+ else:
+ value = [i.strip() for i in value]
+ 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 "%(propname)s": '
+ '"%(value)s" not an entry of %(classname)s')%{
+ 'propname':key, 'value': entry, 'classname': link}
+ l.append(entry)
+ l.sort()
+ value = l
+ elif isinstance(proptype, hyperdb.Boolean):
+ value = form[key].value.strip()
+ props[key] = value = value.lower() in ('yes', 'true', 'on', '1')
+ elif isinstance(proptype, hyperdb.Number):
+ value = form[key].value.strip()
+ props[key] = value = int(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 value != existing:
+ props[key] = value
+ else:
+ props[key] = value
+ return props
+
+
diff --git a/roundup/cgi/templating.py b/roundup/cgi/templating.py
--- /dev/null
@@ -0,0 +1,998 @@
+import sys, cgi, urllib
+
+from roundup import hyperdb, date
+from roundup.i18n import _
+
+
+try:
+ import StructuredText
+except ImportError:
+ StructuredText = None
+
+# Make sure these modules are loaded
+# I need these to run PageTemplates outside of Zope :(
+# If we're running in a Zope environment, these modules will be loaded
+# already...
+if not sys.modules.has_key('zLOG'):
+ import zLOG
+ sys.modules['zLOG'] = zLOG
+if not sys.modules.has_key('MultiMapping'):
+ import MultiMapping
+ sys.modules['MultiMapping'] = MultiMapping
+if not sys.modules.has_key('ComputedAttribute'):
+ import ComputedAttribute
+ sys.modules['ComputedAttribute'] = ComputedAttribute
+if not sys.modules.has_key('ExtensionClass'):
+ import ExtensionClass
+ sys.modules['ExtensionClass'] = ExtensionClass
+if not sys.modules.has_key('Acquisition'):
+ import Acquisition
+ sys.modules['Acquisition'] = Acquisition
+
+# now it's safe to import PageTemplates and ZTUtils
+from PageTemplates import PageTemplate
+import ZTUtils
+
+class RoundupPageTemplate(PageTemplate.PageTemplate):
+ ''' A Roundup-specific PageTemplate.
+
+ Interrogate the client to set up the various template variables to
+ be available:
+
+ *class*
+ The current class of node being displayed as an HTMLClass
+ instance.
+ *item*
+ The current node from the database, if we're viewing a specific
+ node, as an HTMLItem instance. If it doesn't exist, then we're
+ on a new item page.
+ (*classname*)
+ this is one of two things:
+
+ 1. the *item* is also available under its classname, so a *user*
+ node would also be available under the name *user*. This is
+ also an HTMLItem instance.
+ 2. if there's no *item* then the current class is available
+ through this name, thus "user/name" and "user/name/menu" will
+ still work - the latter will pull information from the form
+ if it can.
+ *form*
+ The current CGI form information as a mapping of form argument
+ name to value
+ *request*
+ Includes information about the current request, including:
+ - the url
+ - the current index information (``filterspec``, ``filter`` args,
+ ``properties``, etc) parsed out of the form.
+ - methods for easy filterspec link generation
+ - *user*, the current user node as an HTMLItem instance
+ *instance*
+ The current instance
+ *db*
+ The current database, through which db.config may be reached.
+
+ Maybe also:
+
+ *modules*
+ python modules made available (XXX: not sure what's actually in
+ there tho)
+ '''
+ def __init__(self, client, classname=None, request=None):
+ ''' Extract the vars from the client and install in the context.
+ '''
+ self.client = client
+ self.classname = classname or self.client.classname
+ self.request = request or HTMLRequest(self.client)
+
+ def pt_getContext(self):
+ c = {
+ 'klass': HTMLClass(self.client, self.classname),
+ 'options': {},
+ 'nothing': None,
+ 'request': self.request,
+ 'content': self.client.content,
+ 'db': HTMLDatabase(self.client),
+ 'instance': self.client.instance
+ }
+ # add in the item if there is one
+ if self.client.nodeid:
+ c['item'] = HTMLItem(self.client.db, self.classname,
+ self.client.nodeid)
+ c[self.classname] = c['item']
+ else:
+ c[self.classname] = c['klass']
+ return c
+
+ def render(self, *args, **kwargs):
+ if not kwargs.has_key('args'):
+ kwargs['args'] = args
+ return self.pt_render(extra_context={'options': kwargs})
+
+class HTMLDatabase:
+ ''' Return HTMLClasses for valid class fetches
+ '''
+ def __init__(self, client):
+ self.client = client
+ self.config = client.db.config
+ def __getattr__(self, attr):
+ self.client.db.getclass(attr)
+ return HTMLClass(self.client, attr)
+ def classes(self):
+ l = self.client.db.classes.keys()
+ l.sort()
+ return [HTMLClass(self.client, cn) for cn in l]
+
+class HTMLClass:
+ ''' Accesses through a class (either through *class* or *db.<classname>*)
+ '''
+ def __init__(self, client, classname):
+ self.client = client
+ self.db = client.db
+ self.classname = classname
+ if classname is not None:
+ self.klass = self.db.getclass(self.classname)
+ self.props = self.klass.getprops()
+
+ def __repr__(self):
+ return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
+
+ def __getattr__(self, attr):
+ ''' return an HTMLItem instance'''
+ #print 'getattr', (self, attr)
+ if attr == 'creator':
+ return HTMLUser(self.client)
+
+ if not self.props.has_key(attr):
+ raise AttributeError, attr
+ prop = self.props[attr]
+
+ # look up the correct HTMLProperty class
+ for klass, htmlklass in propclasses:
+ if isinstance(prop, hyperdb.Multilink):
+ value = []
+ else:
+ value = None
+ if isinstance(prop, klass):
+ return htmlklass(self.db, '', prop, attr, value)
+
+ # no good
+ raise AttributeError, attr
+
+ def properties(self):
+ ''' Return HTMLProperty for all props
+ '''
+ l = []
+ for name, prop in self.props.items():
+ for klass, htmlklass in propclasses:
+ if isinstance(prop, hyperdb.Multilink):
+ value = []
+ else:
+ value = None
+ if isinstance(prop, klass):
+ l.append(htmlklass(self.db, '', prop, name, value))
+ return l
+
+ def list(self):
+ l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()]
+ return l
+
+ def filter(self, request=None):
+ ''' Return a list of items from this class, filtered and sorted
+ by the current requested filterspec/filter/sort/group args
+ '''
+ if request is not None:
+ filterspec = request.filterspec
+ sort = request.sort
+ group = request.group
+ l = [HTMLItem(self.db, self.classname, x)
+ for x in self.klass.filter(None, filterspec, sort, group)]
+ return l
+
+ def classhelp(self, properties, label='?', width='400', height='400'):
+ '''pop up a javascript window with class help
+
+ This generates a link to a popup window which displays the
+ properties indicated by "properties" of the class named by
+ "classname". The "properties" should be a comma-separated list
+ (eg. 'id,name,description').
+
+ You may optionally override the label displayed, the width and
+ height. The popup window will be resizable and scrollable.
+ '''
+ return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
+ 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(self.classname,
+ properties, width, height, label)
+
+ def history(self):
+ return 'New node - no history'
+
+ def renderWith(self, name, **kwargs):
+ ''' Render this class with the given template.
+ '''
+ # create a new request and override the specified args
+ req = HTMLRequest(self.client)
+ req.classname = self.classname
+ req.__dict__.update(kwargs)
+
+ # new template, using the specified classname and request
+ pt = RoundupPageTemplate(self.client, self.classname, req)
+
+ # use the specified template
+ name = self.classname + '.' + name
+ pt.write(open('/tmp/test/html/%s'%name).read())
+ pt.id = name
+
+ # XXX handle PT rendering errors here nicely
+ try:
+ return pt.render()
+ except PageTemplate.PTRuntimeError, message:
+ return '<strong>%s</strong><ol>%s</ol>'%(message,
+ cgi.escape('<li>'.join(pt._v_errors)))
+
+class HTMLItem:
+ ''' Accesses through an *item*
+ '''
+ def __init__(self, db, classname, nodeid):
+ self.db = db
+ self.classname = classname
+ self.nodeid = nodeid
+ self.klass = self.db.getclass(classname)
+ self.props = self.klass.getprops()
+
+ def __repr__(self):
+ return '<HTMLItem(0x%x) %s %s>'%(id(self), self.classname, self.nodeid)
+
+ def __getattr__(self, attr):
+ ''' return an HTMLItem instance'''
+ #print 'getattr', (self, attr)
+ if attr == 'id':
+ return self.nodeid
+
+ if not self.props.has_key(attr):
+ raise AttributeError, attr
+ prop = self.props[attr]
+
+ # get the value, handling missing values
+ value = self.klass.get(self.nodeid, attr, None)
+ if value is None:
+ if isinstance(self.props[attr], hyperdb.Multilink):
+ value = []
+
+ # look up the correct HTMLProperty class
+ for klass, htmlklass in propclasses:
+ if isinstance(prop, klass):
+ return htmlklass(self.db, self.nodeid, prop, attr, value)
+
+ # no good
+ raise AttributeError, attr
+
+ # XXX this probably should just return the history items, not the HTML
+ def history(self, direction='descending'):
+ l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
+ '<tr class="list-header">',
+ _('<th align=left><span class="list-item">Date</span></th>'),
+ _('<th align=left><span class="list-item">User</span></th>'),
+ _('<th align=left><span class="list-item">Action</span></th>'),
+ _('<th align=left><span class="list-item">Args</span></th>'),
+ '</tr>']
+ comments = {}
+ history = self.klass.history(self.nodeid)
+ history.sort()
+ if direction == 'descending':
+ history.reverse()
+ for id, evt_date, user, action, args in history:
+ date_s = str(evt_date).replace("."," ")
+ arg_s = ''
+ if action == 'link' and type(args) == type(()):
+ if len(args) == 3:
+ linkcl, linkid, key = args
+ arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
+ linkcl, linkid, key)
+ else:
+ arg_s = str(args)
+
+ elif action == 'unlink' and type(args) == type(()):
+ if len(args) == 3:
+ linkcl, linkid, key = args
+ arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
+ linkcl, linkid, key)
+ else:
+ arg_s = str(args)
+
+ elif type(args) == type({}):
+ cell = []
+ for k in args.keys():
+ # try to get the relevant property and treat it
+ # specially
+ try:
+ prop = props[k]
+ except:
+ prop = None
+ if prop is not None:
+ if args[k] and (isinstance(prop, hyperdb.Multilink) or
+ isinstance(prop, hyperdb.Link)):
+ # figure what the link class is
+ classname = prop.classname
+ try:
+ linkcl = self.db.getclass(classname)
+ except KeyError:
+ labelprop = None
+ comments[classname] = _('''The linked class
+ %(classname)s no longer exists''')%locals()
+ labelprop = linkcl.labelprop(1)
+# hrefable = os.path.exists(
+# os.path.join(self.instance.TEMPLATES,
+# classname+'.item'))
+
+ if isinstance(prop, hyperdb.Multilink) and \
+ len(args[k]) > 0:
+ ml = []
+ for linkid in args[k]:
+ if isinstance(linkid, type(())):
+ sublabel = linkid[0] + ' '
+ linkids = linkid[1]
+ else:
+ sublabel = ''
+ linkids = [linkid]
+ subml = []
+ for linkid in linkids:
+ label = classname + linkid
+ # if we have a label property, try to use it
+ # TODO: test for node existence even when
+ # there's no labelprop!
+ try:
+ if labelprop is not None:
+ label = linkcl.get(linkid, labelprop)
+ except IndexError:
+ comments['no_link'] = _('''<strike>The
+ linked node no longer
+ exists</strike>''')
+ subml.append('<strike>%s</strike>'%label)
+ else:
+# if hrefable:
+ subml.append('<a href="%s%s">%s</a>'%(
+ classname, linkid, label))
+ ml.append(sublabel + ', '.join(subml))
+ cell.append('%s:\n %s'%(k, ', '.join(ml)))
+ elif isinstance(prop, hyperdb.Link) and args[k]:
+ label = classname + args[k]
+ # if we have a label property, try to use it
+ # TODO: test for node existence even when
+ # there's no labelprop!
+ if labelprop is not None:
+ try:
+ label = linkcl.get(args[k], labelprop)
+ except IndexError:
+ comments['no_link'] = _('''<strike>The
+ linked node no longer
+ exists</strike>''')
+ cell.append(' <strike>%s</strike>,\n'%label)
+ # "flag" this is done .... euwww
+ label = None
+ if label is not None:
+ if hrefable:
+ cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
+ classname, args[k], label))
+ else:
+ cell.append('%s: %s' % (k,label))
+
+ elif isinstance(prop, hyperdb.Date) and args[k]:
+ d = date.Date(args[k])
+ cell.append('%s: %s'%(k, str(d)))
+
+ elif isinstance(prop, hyperdb.Interval) and args[k]:
+ d = date.Interval(args[k])
+ cell.append('%s: %s'%(k, str(d)))
+
+ elif isinstance(prop, hyperdb.String) and args[k]:
+ cell.append('%s: %s'%(k, cgi.escape(args[k])))
+
+ elif not args[k]:
+ cell.append('%s: (no value)\n'%k)
+
+ else:
+ cell.append('%s: %s\n'%(k, str(args[k])))
+ else:
+ # property no longer exists
+ comments['no_exist'] = _('''<em>The indicated property
+ no longer exists</em>''')
+ cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
+ arg_s = '<br />'.join(cell)
+ else:
+ # unkown event!!
+ comments['unknown'] = _('''<strong><em>This event is not
+ handled by the history display!</em></strong>''')
+ arg_s = '<strong><em>' + str(args) + '</em></strong>'
+ date_s = date_s.replace(' ', ' ')
+ l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
+ '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
+ user, action, arg_s))
+ if comments:
+ l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
+ for entry in comments.values():
+ l.append('<tr><td colspan=4>%s</td></tr>'%entry)
+ l.append('</table>')
+ return '\n'.join(l)
+
+ def remove(self):
+ # XXX do what?
+ return ''
+
+class HTMLUser(HTMLItem):
+ ''' Accesses through the *user* (a special case of item)
+ '''
+ def __init__(self, client):
+ HTMLItem.__init__(self, client.db, 'user', client.userid)
+ self.default_classname = client.classname
+ self.userid = client.userid
+
+ # used for security checks
+ self.security = client.db.security
+ _marker = []
+ def hasPermission(self, role, classname=_marker):
+ ''' Determine if the user has the Role.
+
+ The class being tested defaults to the template's class, but may
+ be overidden for this test by suppling an alternate classname.
+ '''
+ if classname is self._marker:
+ classname = self.default_classname
+ return self.security.hasPermission(role, self.userid, classname)
+
+class HTMLProperty:
+ ''' String, Number, Date, Interval HTMLProperty
+
+ A wrapper object which may be stringified for the plain() behaviour.
+ '''
+ def __init__(self, db, nodeid, prop, name, value):
+ self.db = db
+ self.nodeid = nodeid
+ self.prop = prop
+ self.name = name
+ self.value = value
+ def __repr__(self):
+ return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self.name, self.prop, self.value)
+ def __str__(self):
+ return self.plain()
+ def __cmp__(self, other):
+ if isinstance(other, HTMLProperty):
+ return cmp(self.value, other.value)
+ return cmp(self.value, other)
+
+class StringHTMLProperty(HTMLProperty):
+ def plain(self, escape=0):
+ if self.value is None:
+ return ''
+ if escape:
+ return cgi.escape(str(self.value))
+ return str(self.value)
+
+ def stext(self, escape=0):
+ s = self.plain(escape=escape)
+ if not StructuredText:
+ return s
+ return StructuredText(s,level=1,header=0)
+
+ def field(self, size = 30):
+ if self.value is None:
+ value = ''
+ else:
+ value = cgi.escape(str(self.value))
+ value = '"'.join(value.split('"'))
+ return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
+
+ def multiline(self, escape=0, rows=5, cols=40):
+ if self.value is None:
+ value = ''
+ else:
+ value = cgi.escape(str(self.value))
+ value = '"'.join(value.split('"'))
+ return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
+ self.name, rows, cols, value)
+
+ def email(self, escape=1):
+ ''' fudge email '''
+ if self.value is None: value = ''
+ else: value = str(self.value)
+ value = value.replace('@', ' at ')
+ value = value.replace('.', ' ')
+ if escape:
+ value = cgi.escape(value)
+ return value
+
+class PasswordHTMLProperty(HTMLProperty):
+ def plain(self):
+ if self.value is None:
+ return ''
+ return _('*encrypted*')
+
+ def field(self, size = 30):
+ return '<input type="password" name="%s" size="%s">'%(self.name, size)
+
+class NumberHTMLProperty(HTMLProperty):
+ def plain(self):
+ return str(self.value)
+
+ def field(self, size = 30):
+ if self.value is None:
+ value = ''
+ else:
+ value = cgi.escape(str(self.value))
+ value = '"'.join(value.split('"'))
+ return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
+
+class BooleanHTMLProperty(HTMLProperty):
+ def plain(self):
+ if self.value is None:
+ return ''
+ return self.value and "Yes" or "No"
+
+ def field(self):
+ checked = self.value and "checked" or ""
+ s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self.name,
+ checked)
+ if checked:
+ checked = ""
+ else:
+ checked = "checked"
+ s += '<input type="radio" name="%s" value="no" %s>No'%(self.name,
+ checked)
+ return s
+
+class DateHTMLProperty(HTMLProperty):
+ def plain(self):
+ if self.value is None:
+ return ''
+ return str(self.value)
+
+ def field(self, size = 30):
+ if self.value is None:
+ value = ''
+ else:
+ value = cgi.escape(str(self.value))
+ value = '"'.join(value.split('"'))
+ return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
+
+ def reldate(self, pretty=1):
+ if not self.value:
+ return ''
+
+ # figure the interval
+ interval = date.Date('.') - self.value
+ if pretty:
+ return interval.pretty()
+ return str(interval)
+
+class IntervalHTMLProperty(HTMLProperty):
+ def plain(self):
+ if self.value is None:
+ return ''
+ return str(self.value)
+
+ def pretty(self):
+ return self.value.pretty()
+
+ def field(self, size = 30):
+ if self.value is None:
+ value = ''
+ else:
+ value = cgi.escape(str(self.value))
+ value = '"'.join(value.split('"'))
+ return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
+
+class LinkHTMLProperty(HTMLProperty):
+ ''' Link HTMLProperty
+ Include the above as well as being able to access the class
+ information. Stringifying the object itself results in the value
+ from the item being displayed. Accessing attributes of this object
+ result in the appropriate entry from the class being queried for the
+ property accessed (so item/assignedto/name would look up the user
+ entry identified by the assignedto property on item, and then the
+ name property of that user)
+ '''
+ def __getattr__(self, attr):
+ ''' return a new HTMLItem '''
+ #print 'getattr', (self, attr, self.value)
+ if not self.value:
+ raise AttributeError, "Can't access missing value"
+ i = HTMLItem(self.db, self.prop.classname, self.value)
+ return getattr(i, attr)
+
+ def plain(self, escape=0):
+ if self.value is None:
+ return _('[unselected]')
+ linkcl = self.db.classes[self.klass.classname]
+ k = linkcl.labelprop(1)
+ value = str(linkcl.get(self.value, k))
+ if escape:
+ value = cgi.escape(value)
+ return value
+
+ # XXX most of the stuff from here down is of dubious utility - it's easy
+ # enough to do in the template by hand (and in some cases, it's shorter
+ # and clearer...
+
+ def field(self):
+ linkcl = self.db.getclass(self.prop.classname)
+ if linkcl.getprops().has_key('order'):
+ sort_on = 'order'
+ else:
+ sort_on = linkcl.labelprop()
+ options = linkcl.filter(None, {}, [sort_on], [])
+ # TODO: make this a field display, not a menu one!
+ l = ['<select name="%s">'%property]
+ k = linkcl.labelprop(1)
+ if value is None:
+ s = 'selected '
+ else:
+ s = ''
+ l.append(_('<option %svalue="-1">- no selection -</option>')%s)
+ for optionid in options:
+ option = linkcl.get(optionid, k)
+ s = ''
+ if optionid == value:
+ s = 'selected '
+ if showid:
+ lab = '%s%s: %s'%(self.prop.classname, optionid, option)
+ else:
+ lab = option
+ if size is not None and len(lab) > size:
+ lab = lab[:size-3] + '...'
+ lab = cgi.escape(lab)
+ l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
+ l.append('</select>')
+ return '\n'.join(l)
+
+ def download(self, showid=0):
+ linkname = self.prop.classname
+ linkcl = self.db.getclass(linkname)
+ k = linkcl.labelprop(1)
+ linkvalue = cgi.escape(str(linkcl.get(self.value, k)))
+ if showid:
+ label = value
+ title = ' title="%s"'%linkvalue
+ # note ... this should be urllib.quote(linkcl.get(value, k))
+ else:
+ label = linkvalue
+ title = ''
+ return '<a href="%s%s/%s"%s>%s</a>'%(linkname, self.value,
+ linkvalue, title, label)
+
+ def menu(self, size=None, height=None, showid=0, additional=[],
+ **conditions):
+ value = self.value
+
+ # sort function
+ sortfunc = make_sort_function(self.db, self.prop.classname)
+
+ # force the value to be a single choice
+ if isinstance(value, type('')):
+ value = value[0]
+ linkcl = self.db.getclass(self.prop.classname)
+ l = ['<select name="%s">'%self.name]
+ k = linkcl.labelprop(1)
+ s = ''
+ if value is None:
+ s = 'selected '
+ l.append(_('<option %svalue="-1">- no selection -</option>')%s)
+ if linkcl.getprops().has_key('order'):
+ sort_on = 'order'
+ else:
+ sort_on = linkcl.labelprop()
+ options = linkcl.filter(None, conditions, [sort_on], [])
+ for optionid in options:
+ option = linkcl.get(optionid, k)
+ s = ''
+ if value in [optionid, option]:
+ s = 'selected '
+ if showid:
+ lab = '%s%s: %s'%(self.prop.classname, optionid, option)
+ else:
+ lab = option
+ if size is not None and len(lab) > size:
+ lab = lab[:size-3] + '...'
+ if additional:
+ m = []
+ for propname in additional:
+ m.append(linkcl.get(optionid, propname))
+ lab = lab + ' (%s)'%', '.join(map(str, m))
+ lab = cgi.escape(lab)
+ l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
+ l.append('</select>')
+ return '\n'.join(l)
+
+# def checklist(self, ...)
+
+class MultilinkHTMLProperty(HTMLProperty):
+ ''' Multilink HTMLProperty
+
+ Also be iterable, returning a wrapper object like the Link case for
+ each entry in the multilink.
+ '''
+ def __len__(self):
+ ''' length of the multilink '''
+ return len(self.value)
+
+ def __getattr__(self, attr):
+ ''' no extended attribute accesses make sense here '''
+ raise AttributeError, attr
+
+ def __getitem__(self, num):
+ ''' iterate and return a new HTMLItem '''
+ #print 'getitem', (self, num)
+ value = self.value[num]
+ return HTMLItem(self.db, self.prop.classname, value)
+
+ def plain(self, escape=0):
+ linkcl = self.db.classes[self.prop.classname]
+ k = linkcl.labelprop(1)
+ labels = []
+ for v in self.value:
+ labels.append(linkcl.get(v, k))
+ value = ', '.join(labels)
+ if escape:
+ value = cgi.escape(value)
+ return value
+
+ # XXX most of the stuff from here down is of dubious utility - it's easy
+ # enough to do in the template by hand (and in some cases, it's shorter
+ # and clearer...
+
+ def field(self, size=30, showid=0):
+ sortfunc = make_sort_function(self.db, self.prop.classname)
+ linkcl = self.db.getclass(self.prop.classname)
+ value = self.value[:]
+ if value:
+ value.sort(sortfunc)
+ # map the id to the label property
+ if not showid:
+ k = linkcl.labelprop(1)
+ value = [linkcl.get(v, k) for v in value]
+ value = cgi.escape(','.join(value))
+ return '<input name="%s" size="%s" value="%s">'%(self.name, size, value)
+
+ def menu(self, size=None, height=None, showid=0, additional=[],
+ **conditions):
+ value = self.value
+
+ # sort function
+ sortfunc = make_sort_function(self.db, self.prop.classname)
+
+ linkcl = self.db.getclass(self.prop.classname)
+ if linkcl.getprops().has_key('order'):
+ sort_on = 'order'
+ else:
+ sort_on = linkcl.labelprop()
+ options = linkcl.filter(None, conditions, [sort_on], [])
+ height = height or min(len(options), 7)
+ l = ['<select multiple name="%s" size="%s">'%(self.name, height)]
+ k = linkcl.labelprop(1)
+ for optionid in options:
+ option = linkcl.get(optionid, k)
+ s = ''
+ if optionid in value or option in value:
+ s = 'selected '
+ if showid:
+ lab = '%s%s: %s'%(self.prop.classname, optionid, option)
+ else:
+ lab = option
+ if size is not None and len(lab) > size:
+ lab = lab[:size-3] + '...'
+ if additional:
+ m = []
+ for propname in additional:
+ m.append(linkcl.get(optionid, propname))
+ lab = lab + ' (%s)'%', '.join(m)
+ lab = cgi.escape(lab)
+ l.append('<option %svalue="%s">%s</option>'%(s, optionid,
+ lab))
+ l.append('</select>')
+ return '\n'.join(l)
+
+# set the propclasses for HTMLItem
+propclasses = (
+ (hyperdb.String, StringHTMLProperty),
+ (hyperdb.Number, NumberHTMLProperty),
+ (hyperdb.Boolean, BooleanHTMLProperty),
+ (hyperdb.Date, DateHTMLProperty),
+ (hyperdb.Interval, IntervalHTMLProperty),
+ (hyperdb.Password, PasswordHTMLProperty),
+ (hyperdb.Link, LinkHTMLProperty),
+ (hyperdb.Multilink, MultilinkHTMLProperty),
+)
+
+def make_sort_function(db, classname):
+ '''Make a sort function for a given class
+ '''
+ linkcl = db.getclass(classname)
+ if linkcl.getprops().has_key('order'):
+ sort_on = 'order'
+ else:
+ sort_on = linkcl.labelprop()
+ def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
+ return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
+ return sortfunc
+
+def handleListCGIValue(value):
+ ''' Value is either a single item or a list of items. Each item has a
+ .value that we're actually interested in.
+ '''
+ if isinstance(value, type([])):
+ return [value.value for value in value]
+ else:
+ return value.value.split(',')
+
+# XXX This is starting to look a lot (in data terms) like the client object
+# itself!
+class HTMLRequest:
+ ''' The *request*, holding the CGI form and environment.
+
+ '''
+ def __init__(self, client):
+ self.client = client
+
+ # easier access vars
+ self.form = client.form
+ self.env = client.env
+ self.base = client.base
+ self.user = HTMLUser(client)
+
+ # store the current class name and action
+ self.classname = client.classname
+ self.template_type = client.template_type
+
+ # extract the index display information from the form
+ self.columns = {}
+ if self.form.has_key(':columns'):
+ for entry in handleListCGIValue(self.form[':columns']):
+ self.columns[entry] = 1
+ self.sort = []
+ if self.form.has_key(':sort'):
+ self.sort = handleListCGIValue(self.form[':sort'])
+ self.group = []
+ if self.form.has_key(':group'):
+ self.group = handleListCGIValue(self.form[':group'])
+ self.filter = []
+ if self.form.has_key(':filter'):
+ self.filter = handleListCGIValue(self.form[':filter'])
+ self.filterspec = {}
+ for name in self.filter:
+ if self.form.has_key(name):
+ self.filterspec[name]=handleListCGIValue(self.form[name])
+
+ def __str__(self):
+ d = {}
+ d.update(self.__dict__)
+ f = ''
+ for k in self.form.keys():
+ f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
+ d['form'] = f
+ e = ''
+ for k,v in self.env.items():
+ e += '\n %r=%r'%(k, v)
+ d['env'] = e
+ return '''
+form: %(form)s
+base: %(base)r
+classname: %(classname)r
+template_type: %(template_type)r
+columns: %(columns)r
+sort: %(sort)r
+group: %(group)r
+filter: %(filter)r
+filterspec: %(filterspec)r
+env: %(env)s
+'''%d
+
+ def indexargs_form(self):
+ ''' return the current index args as form elements '''
+ l = []
+ s = '<input type="hidden" name="%s" value="%s">'
+ if self.columns:
+ l.append(s%(':columns', ','.join(self.columns.keys())))
+ if self.sort:
+ l.append(s%(':sort', ','.join(self.sort)))
+ if self.group:
+ l.append(s%(':group', ','.join(self.group)))
+ if self.filter:
+ l.append(s%(':filter', ','.join(self.filter)))
+ for k,v in self.filterspec.items():
+ l.append(s%(k, ','.join(v)))
+ return '\n'.join(l)
+
+ def indexargs_href(self, url, args):
+ l = ['%s=%s'%(k,v) for k,v in args.items()]
+ if self.columns:
+ l.append(':columns=%s'%(','.join(self.columns.keys())))
+ if self.sort:
+ l.append(':sort=%s'%(','.join(self.sort)))
+ if self.group:
+ l.append(':group=%s'%(','.join(self.group)))
+ if self.filter:
+ l.append(':filter=%s'%(','.join(self.filter)))
+ for k,v in self.filterspec.items():
+ l.append('%s=%s'%(k, ','.join(v)))
+ return '%s?%s'%(url, '&'.join(l))
+
+ def base_javascript(self):
+ return '''
+<script language="javascript">
+submitted = false;
+function submit_once() {
+ if (submitted) {
+ alert("Your request is being processed.\\nPlease be patient.");
+ return 0;
+ }
+ submitted = true;
+ return 1;
+}
+
+function help_window(helpurl, width, height) {
+ HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
+}
+</script>
+'''%self.base
+
+ def batch(self):
+ ''' Return a batch object for results from the "current search"
+ '''
+ filterspec = self.filterspec
+ sort = self.sort
+ group = self.group
+
+ # get the list of ids we're batching over
+ klass = self.client.db.getclass(self.classname)
+ l = klass.filter(None, filterspec, sort, group)
+
+ # figure batch args
+ if self.form.has_key(':pagesize'):
+ size = int(self.form[':pagesize'].value)
+ else:
+ size = 50
+ if self.form.has_key(':startwith'):
+ start = int(self.form[':startwith'].value)
+ else:
+ start = 0
+
+ # return the batch object
+ return Batch(self.client, self.classname, l, size, start)
+
+class Batch(ZTUtils.Batch):
+ def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
+ self.client = client
+ self.classname = classname
+ ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
+
+ # overwrite so we can late-instantiate the HTMLItem instance
+ def __getitem__(self, index):
+ if index < 0:
+ if index + self.end < self.first: raise IndexError, index
+ return self._sequence[index + self.end]
+
+ if index >= self.length: raise IndexError, index
+
+ # wrap the return in an HTMLItem
+ return HTMLItem(self.client.db, self.classname,
+ self._sequence[index+self.first])
+
+ # override these 'cos we don't have access to acquisition
+ def previous(self):
+ print self.start
+ if self.start == 1:
+ return None
+ return Batch(self.client, self.classname, self._sequence, self._size,
+ self.first - self._size + self.overlap, 0, self.orphan,
+ self.overlap)
+
+ def next(self):
+ try:
+ self._sequence[self.end]
+ except IndexError:
+ return None
+ return Batch(self.client, self.classname, self._sequence, self._size,
+ self.end - self.overlap, 0, self.orphan, self.overlap)
+
+ def length(self):
+ self.sequence_length = l = len(self._sequence)
+ return l
+
diff --git a/roundup/cgi/zLOG.py b/roundup/cgi/zLOG.py
--- /dev/null
+++ b/roundup/cgi/zLOG.py
@@ -0,0 +1,2 @@
+def LOG(*args, **kw):
+ pass