From: richard Date: Fri, 30 Aug 2002 08:28:44 +0000 (+0000) Subject: New CGI interface support X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=edcaadf85fe6ee29a8a374495fadaf380667dbcf;p=roundup.git New CGI interface support git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1002 57a73879-2fb5-44c3-a270-3262357dd7e2 --- diff --git a/roundup/cgi/Acquisition.py b/roundup/cgi/Acquisition.py new file mode 100644 index 0000000..bb5f227 --- /dev/null +++ b/roundup/cgi/Acquisition.py @@ -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 new file mode 100644 index 0000000..7117fb4 --- /dev/null +++ b/roundup/cgi/ComputedAttribute.py @@ -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 new file mode 100644 index 0000000..764e53e --- /dev/null +++ b/roundup/cgi/ExtensionClass.py @@ -0,0 +1,2 @@ +class Base: + pass diff --git a/roundup/cgi/MultiMapping.py b/roundup/cgi/MultiMapping.py new file mode 100644 index 0000000..b528288 --- /dev/null +++ b/roundup/cgi/MultiMapping.py @@ -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 new file mode 100644 index 0000000..e69de29 diff --git a/roundup/cgi/cgitb.py b/roundup/cgi/cgitb.py new file mode 100644 index 0000000..21f1942 --- /dev/null +++ b/roundup/cgi/cgitb.py @@ -0,0 +1,161 @@ +# +# This module was written by Ka-Ping Yee, . +# +# $Id: cgitb.py,v 1.1 2002-08-30 08:28:44 richard Exp $ + +__doc__ = """ +Extended CGI traceback handler by Ka-Ping Yee, . +""" + +import sys, os, types, string, keyword, linecache, tokenize, inspect, pydoc + +from roundup.i18n import _ + +def breaker(): + return ('' + + ' > ' + + '' * 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] + '
' + sys.executable + head = pydoc.html.heading( + '%s: %s'%(etype, evalue), + '#ffffff', '#777777', pyver) + + head = head + (_('

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 = '%s ' % (' ' * 5) + traceback = [] + for frame, file, lnum, func, lines, index in inspect.trace(context): + if file is None: + link = '<file is None - probably inside eval or exec>' + else: + file = os.path.abspath(file) + link = '%s' % (file, pydoc.html.escape(file)) + args, varargs, varkw, locals = inspect.getargvalues(frame) + if func == '?': + call = '' + else: + call = 'in %s' % func + inspect.formatargvalues( + args, varargs, varkw, locals, + formatvalue=lambda value: '=' + pydoc.html.repr(value)) + + level = ''' + +
%s %s
''' % (link, call) + + if index is None or file is None: + traceback.append('

' + 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 = _('undefined') + name = '%s' % name + else: + if frame.f_globals.has_key(name): + value = pydoc.html.repr(frame.f_globals[name]) + else: + value = _('undefined') + name = 'global %s' % name + lvals.append('%s = %s' % (name, value)) + if lvals: + lvals = string.join(lvals, ', ') + lvals = indent + ''' +%s
''' % lvals + else: + lvals = '' + + excerpt = [] + i = lnum - index + for line in lines: + number = ' ' * (5-len(str(i))) + str(i) + number = '%s' % number + line = '%s %s' % (number, pydoc.html.preformat(line)) + if i == lnum: + line = ''' + +
%s
''' % line + excerpt.append('\n' + line) + if i == lnum: + excerpt.append(lvals) + i = i + 1 + traceback.append('

' + level + string.join(excerpt, '\n')) + + traceback.reverse() + + exception = '

%s: %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('
%s%s = %s' % (indent, name, value)) + + return head + string.join(attribs) + string.join(traceback) + '

 

' + +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 new file mode 100644 index 0000000..bcc0a16 --- /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 '%s
    %s
'%(message, + cgi.escape('
  • '.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('
    %s
    '%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 %s' % (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('
    %s
    '%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.
    \n' + 'Get it from: 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 new file mode 100644 index 0000000..d109d88 --- /dev/null +++ b/roundup/cgi/templating.py @@ -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.*) + ''' + 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 ''%(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 '(%s)'%(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 '%s
      %s
    '%(message, + cgi.escape('
  • '.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 ''%(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 = ['', + '', + _(''), + _(''), + _(''), + _(''), + ''] + 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 += '%s%s %s'%(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 += '%s%s %s'%(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'] = _('''The + linked node no longer + exists''') + subml.append('%s'%label) + else: +# if hrefable: + subml.append('%s'%( + 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'] = _('''The + linked node no longer + exists''') + cell.append(' %s,\n'%label) + # "flag" this is done .... euwww + label = None + if label is not None: + if hrefable: + cell.append('%s: %s\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'] = _('''The indicated property + no longer exists''') + cell.append('%s: %s\n'%(k, str(args[k]))) + arg_s = '
    '.join(cell) + else: + # unkown event!! + comments['unknown'] = _('''This event is not + handled by the history display!''') + arg_s = '' + str(args) + '' + date_s = date_s.replace(' ', ' ') + l.append('' + ''%(date_s, + user, action, arg_s)) + if comments: + l.append(_('')) + for entry in comments.values(): + l.append(''%entry) + l.append('
    DateUserActionArgs
    %s%s%s%s
    Note:
    %s
    ') + 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 ''%(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 ''%(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 ''%( + 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 ''%(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 ''%(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 = 'Yes'%(self.name, + checked) + if checked: + checked = "" + else: + checked = "checked" + 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 ''%(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 ''%(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 = ['') + 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 '%s'%(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 = ['') + 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 ''%(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 = ['') + 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 = '' + 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 ''' + +'''%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 new file mode 100644 index 0000000..cb13192 --- /dev/null +++ b/roundup/cgi/zLOG.py @@ -0,0 +1,2 @@ +def LOG(*args, **kw): + pass