Mercurial > p > roundup > code
diff roundup/cgi/templating.py @ 1356:83f33642d220 maint-0.5
[[Metadata associated with this commit was garbled during conversion from CVS
to Subversion.]]
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Thu, 09 Jan 2003 22:59:22 +0000 |
| parents | |
| children | 56c5b4509378 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/roundup/cgi/templating.py Thu Jan 09 22:59:22 2003 +0000 @@ -0,0 +1,1639 @@ +import sys, cgi, urllib, os, re, os.path, time, errno + +from roundup import hyperdb, date +from roundup.i18n import _ + +try: + import cPickle as pickle +except ImportError: + import pickle +try: + import cStringIO as StringIO +except ImportError: + import StringIO +try: + import StructuredText +except ImportError: + StructuredText = None + +# bring in the templating support +from roundup.cgi.PageTemplates import PageTemplate +from roundup.cgi.PageTemplates.Expressions import getEngine +from roundup.cgi.TAL.TALInterpreter import TALInterpreter +from roundup.cgi import ZTUtils + +class NoTemplate(Exception): + pass + +class Templates: + templates = {} + + def __init__(self, dir): + self.dir = dir + + def precompileTemplates(self): + ''' Go through a directory and precompile all the templates therein + ''' + for filename in os.listdir(self.dir): + if os.path.isdir(filename): continue + if '.' in filename: + name, extension = filename.split('.') + self.getTemplate(name, extension) + else: + self.getTemplate(filename, None) + + def get(self, name, extension): + ''' Interface to get a template, possibly loading a compiled template. + + "name" and "extension" indicate the template we're after, which in + most cases will be "name.extension". If "extension" is None, then + we look for a template just called "name" with no extension. + + If the file "name.extension" doesn't exist, we look for + "_generic.extension" as a fallback. + ''' + # default the name to "home" + if name is None: + name = 'home' + + # find the source, figure the time it was last modified + if extension: + filename = '%s.%s'%(name, extension) + else: + filename = name + src = os.path.join(self.dir, filename) + try: + stime = os.stat(src)[os.path.stat.ST_MTIME] + except os.error, error: + if error.errno != errno.ENOENT: + raise + if not extension: + raise NoTemplate, 'Template file "%s" doesn\'t exist'%name + + # try for a generic template + generic = '_generic.%s'%extension + src = os.path.join(self.dir, generic) + try: + stime = os.stat(src)[os.path.stat.ST_MTIME] + except os.error, error: + if error.errno != errno.ENOENT: + raise + # nicer error + raise NoTemplate, 'No template file exists for templating '\ + '"%s" with template "%s" (neither "%s" nor "%s")'%(name, + extension, filename, generic) + filename = generic + + if self.templates.has_key(src) and \ + stime < self.templates[src].mtime: + # compiled template is up to date + return self.templates[src] + + # compile the template + self.templates[src] = pt = RoundupPageTemplate() + pt.write(open(src).read()) + pt.id = filename + pt.mtime = time.time() + return pt + + def __getitem__(self, name): + name, extension = os.path.splitext(name) + if extension: + extension = extension[1:] + try: + return self.get(name, extension) + except NoTemplate, message: + raise KeyError, message + +class RoundupPageTemplate(PageTemplate.PageTemplate): + ''' A Roundup-specific PageTemplate. + + Interrogate the client to set up the various template variables to + be available: + + *context* + this is one of three things: + 1. None - we're viewing a "home" page + 2. The current class of item being displayed. This is an HTMLClass + instance. + 3. The current item from the database, if we're viewing a specific + item, as an HTMLItem instance. + *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 + - *form*, the current CGI form information as a FieldStorage + *config* + The current tracker config. + *db* + The current database, used to access arbitrary database items. + *utils* + This is a special class that has its base in the TemplatingUtils + class in this file. If the tracker interfaces module defines a + TemplatingUtils class then it is mixed in, overriding the methods + in the base class. + ''' + def getContext(self, client, classname, request): + # construct the TemplatingUtils class + utils = TemplatingUtils + if hasattr(client.instance.interfaces, 'TemplatingUtils'): + class utils(client.instance.interfaces.TemplatingUtils, utils): + pass + + c = { + 'options': {}, + 'nothing': None, + 'request': request, + 'db': HTMLDatabase(client), + 'config': client.instance.config, + 'tracker': client.instance, + 'utils': utils(client), + 'templates': Templates(client.instance.config.TEMPLATES), + } + # add in the item if there is one + if client.nodeid: + if classname == 'user': + c['context'] = HTMLUser(client, classname, client.nodeid) + else: + c['context'] = HTMLItem(client, classname, client.nodeid) + elif client.db.classes.has_key(classname): + c['context'] = HTMLClass(client, classname) + return c + + def render(self, client, classname, request, **options): + """Render this Page Template""" + + if not self._v_cooked: + self._cook() + + __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self) + + if self._v_errors: + raise PageTemplate.PTRuntimeError, \ + 'Page Template %s has errors.'%self.id + + # figure the context + classname = classname or client.classname + request = request or HTMLRequest(client) + c = self.getContext(client, classname, request) + c.update({'options': options}) + + # and go + output = StringIO.StringIO() + TALInterpreter(self._v_program, self.macros, + getEngine().getContext(c), output, tal=1, strictinsert=0)() + return output.getvalue() + +class HTMLDatabase: + ''' Return HTMLClasses for valid class fetches + ''' + def __init__(self, client): + self._client = client + + # we want config to be exposed + self.config = client.db.config + + def __getitem__(self, item): + self._client.db.getclass(item) + return HTMLClass(self._client, item) + + def __getattr__(self, attr): + try: + return self[attr] + except KeyError: + raise AttributeError, attr + + def classes(self): + l = self._client.db.classes.keys() + l.sort() + return [HTMLClass(self._client, cn) for cn in l] + +def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')): + cl = db.getclass(prop.classname) + l = [] + for entry in ids: + if num_re.match(entry): + l.append(entry) + else: + try: + l.append(cl.lookup(entry)) + except KeyError: + # ignore invalid keys + pass + return l + +class HTMLPermissions: + ''' Helpers that provide answers to commonly asked Permission questions. + ''' + def is_edit_ok(self): + ''' Is the user allowed to Edit the current class? + ''' + return self._db.security.hasPermission('Edit', self._client.userid, + self._classname) + def is_view_ok(self): + ''' Is the user allowed to View the current class? + ''' + return self._db.security.hasPermission('View', self._client.userid, + self._classname) + def is_only_view_ok(self): + ''' Is the user only allowed to View (ie. not Edit) the current class? + ''' + return self.is_view_ok() and not self.is_edit_ok() + +class HTMLClass(HTMLPermissions): + ''' Accesses through a class (either through *class* or *db.<classname>*) + ''' + def __init__(self, client, classname): + self._client = client + self._db = client.db + + # we want classname to be exposed, but _classname gives a + # consistent API for extending Class/Item + self._classname = self.classname = classname + 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 __getitem__(self, item): + ''' return an HTMLProperty instance + ''' + #print 'HTMLClass.getitem', (self, item) + + # we don't exist + if item == 'id': + return None + + # get the property + prop = self._props[item] + + # look up the correct HTMLProperty class + form = self._client.form + for klass, htmlklass in propclasses: + if not isinstance(prop, klass): + continue + if form.has_key(item): + if isinstance(prop, hyperdb.Multilink): + value = lookupIds(self._db, prop, + handleListCGIValue(form[item])) + elif isinstance(prop, hyperdb.Link): + value = form[item].value.strip() + if value: + value = lookupIds(self._db, prop, [value])[0] + else: + value = None + else: + value = form[item].value.strip() or None + else: + if isinstance(prop, hyperdb.Multilink): + value = [] + else: + value = None + return htmlklass(self._client, '', prop, item, value) + + # no good + raise KeyError, item + + def __getattr__(self, attr): + ''' convenience access ''' + try: + return self[attr] + except KeyError: + raise AttributeError, attr + + def getItem(self, itemid, num_re=re.compile('\d+')): + ''' Get an item of this class by its item id. + ''' + # make sure we're looking at an itemid + if not num_re.match(itemid): + itemid = self._klass.lookup(itemid) + + if self.classname == 'user': + klass = HTMLUser + else: + klass = HTMLItem + + return klass(self._client, self.classname, itemid) + + def properties(self): + ''' Return HTMLProperty for all of this class' properties. + ''' + 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._client, '', prop, name, value)) + return l + + def list(self): + ''' List all items in this class. + ''' + if self.classname == 'user': + klass = HTMLUser + else: + klass = HTMLItem + + # get the list and sort it nicely + l = self._klass.list() + sortfunc = make_sort_function(self._db, self.classname) + l.sort(sortfunc) + + l = [klass(self._client, self.classname, x) for x in l] + return l + + def csv(self): + ''' Return the items of this class as a chunk of CSV text. + ''' + # get the CSV module + try: + import csv + except ImportError: + return 'Sorry, you need the csv module to use this function.\n'\ + 'Get it from: http://www.object-craft.com.au/projects/csv/' + + props = self.propnames() + p = csv.parser() + s = StringIO.StringIO() + s.write(p.join(props) + '\n') + for nodeid in self._klass.list(): + l = [] + for name in props: + value = self._klass.get(nodeid, name) + if value is None: + l.append('') + elif isinstance(value, type([])): + l.append(':'.join(map(str, value))) + else: + l.append(str(self._klass.get(nodeid, name))) + s.write(p.join(l) + '\n') + return s.getvalue() + + def propnames(self): + ''' Return the list of the names of the properties of this class. + ''' + idlessprops = self._klass.getprops(protected=0).keys() + idlessprops.sort() + return ['id'] + idlessprops + + 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 + if self.classname == 'user': + klass = HTMLUser + else: + klass = HTMLItem + l = [klass(self._client, self.classname, x) + for x in self._klass.filter(None, filterspec, sort, group)] + return l + + def classhelp(self, properties=None, label='list', width='500', + 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'). Properties defaults to all the + properties of a class (excluding id, creator, created and + activity). + + You may optionally override the label displayed, the width and + height. The popup window will be resizable and scrollable. + ''' + if properties is None: + properties = self._klass.getprops(protected=0).keys() + properties.sort() + properties = ','.join(properties) + return '<a href="javascript:help_window(\'%s?:template=help&' \ + 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%( + self.classname, properties, width, height, label) + + def submit(self, label="Submit New Entry"): + ''' Generate a submit button (and action hidden element) + ''' + return ' <input type="hidden" name=":action" value="new">\n'\ + ' <input type="submit" name="submit" value="%s">'%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.update(kwargs) + + # new template, using the specified classname and request + pt = Templates(self._db.config.TEMPLATES).get(self.classname, name) + + # use our fabricated request + return pt.render(self._client, self.classname, req) + +class HTMLItem(HTMLPermissions): + ''' Accesses through an *item* + ''' + def __init__(self, client, classname, nodeid): + self._client = client + self._db = client.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 __getitem__(self, item): + ''' return an HTMLProperty instance + ''' + #print 'HTMLItem.getitem', (self, item) + if item == 'id': + return self._nodeid + + # get the property + prop = self._props[item] + + # get the value, handling missing values + value = self._klass.get(self._nodeid, item, None) + if value is None: + if isinstance(self._props[item], hyperdb.Multilink): + value = [] + + # look up the correct HTMLProperty class + for klass, htmlklass in propclasses: + if isinstance(prop, klass): + return htmlklass(self._client, self._nodeid, prop, item, value) + + raise KeyErorr, item + + def __getattr__(self, attr): + ''' convenience access to properties ''' + try: + return self[attr] + except KeyError: + raise AttributeError, attr + + def submit(self, label="Submit Changes"): + ''' Generate a submit button (and action hidden element) + ''' + return ' <input type="hidden" name=":action" value="edit">\n'\ + ' <input type="submit" name="submit" value="%s">'%label + + def journal(self, direction='descending'): + ''' Return a list of HTMLJournalEntry instances. + ''' + # XXX do this + return [] + + def history(self, direction='descending', dre=re.compile('\d+')): + l = ['<table class="history">' + '<tr><th colspan="4" class="header">', + _('History'), + '</th></tr><tr>', + _('<th>Date</th>'), + _('<th>User</th>'), + _('<th>Action</th>'), + _('<th>Args</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 = self._props[k] + except KeyError: + 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._db.config.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 and \ + labelprop != 'id': + 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)) + else: + subml.append(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 and labelprop != 'id': + 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(' ', ' ') + # if the user's an itemid, figure the username (older journals + # have the username) + if dre.match(user): + user = self._db.user.get(user, 'username') + l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%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 renderQueryForm(self): + ''' Render this item, which is a query, as a search form. + ''' + # create a new request and override the specified args + req = HTMLRequest(self._client) + req.classname = self._klass.get(self._nodeid, 'klass') + name = self._klass.get(self._nodeid, 'name') + req.updateFromURL(self._klass.get(self._nodeid, 'url') + + '&:queryname=%s'%urllib.quote(name)) + + # new template, using the specified classname and request + pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search') + + # use our fabricated request + return pt.render(self._client, req.classname, req) + +class HTMLUser(HTMLItem): + ''' Accesses through the *user* (a special case of item) + ''' + def __init__(self, client, classname, nodeid): + HTMLItem.__init__(self, client, 'user', nodeid) + self._default_classname = client.classname + + # 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._nodeid, classname) + + def is_edit_ok(self): + ''' Is the user allowed to Edit the current class? + Also check whether this is the current user's info. + ''' + return self._db.security.hasPermission('Edit', self._client.userid, + self._classname) or self._nodeid == self._client.userid + + def is_view_ok(self): + ''' Is the user allowed to View the current class? + Also check whether this is the current user's info. + ''' + return self._db.security.hasPermission('Edit', self._client.userid, + self._classname) or self._nodeid == self._client.userid + +class HTMLProperty: + ''' String, Number, Date, Interval HTMLProperty + + Has useful attributes: + + _name the name of the property + _value the value of the property if any + + A wrapper object which may be stringified for the plain() behaviour. + ''' + def __init__(self, client, nodeid, prop, name, value): + self._client = client + self._db = client.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): + url_re = re.compile(r'\w{3,6}://\S+') + email_re = re.compile(r'\w+@[\w\.\-]+') + designator_re = re.compile(r'([a-z_]+)(\d+)') + def _url_repl(self, match): + s = match.group(0) + return '<a href="%s">%s</a>'%(s, s) + def _email_repl(self, match): + s = match.group(0) + return '<a href="mailto:%s">%s</a>'%(s, s) + def _designator_repl(self, match): + s = match.group(0) + s1 = match.group(1) + s2 = match.group(2) + try: + # make sure s1 is a valid tracker classname + self._db.getclass(s1) + return '<a href="%s">%s %s</a>'%(s, s1, s2) + except KeyError: + return '%s%s'%(s1, s2) + + def plain(self, escape=0, hyperlink=1): + ''' Render a "plain" representation of the property + + "escape" turns on/off HTML quoting + "hyperlink" turns on/off in-text hyperlinking of URLs, email + addresses and designators + ''' + if self._value is None: + return '' + if escape: + s = cgi.escape(str(self._value)) + else: + s = self._value + if hyperlink: + s = self.url_re.sub(self._url_repl, s) + s = self.email_re.sub(self._email_repl, s) + s = self.designator_re.sub(self._designator_repl, s) + return s + + def stext(self, escape=0): + ''' Render the value of the property as StructuredText. + + This requires the StructureText module to be installed separately. + ''' + s = self.plain(escape=escape) + if not StructuredText: + return s + return StructuredText(s,level=1,header=0) + + def field(self, size = 30): + ''' Render a form edit field for the property + ''' + 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): + ''' Render a multiline form edit field for the property + ''' + 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): + ''' Render the value of the property as an obscured email address + ''' + if self._value is None: value = '' + else: value = str(self._value) + if value.find('@') != -1: + name, domain = value.split('@') + domain = ' '.join(domain.split('.')[:-1]) + name = name.replace('.', ' ') + value = '%s at %s ...'%(name, domain) + else: + value = value.replace('.', ' ') + if escape: + value = cgi.escape(value) + return value + +class PasswordHTMLProperty(HTMLProperty): + def plain(self): + ''' Render a "plain" representation of the property + ''' + if self._value is None: + return '' + return _('*encrypted*') + + def field(self, size = 30): + ''' Render a form edit field for the property. + ''' + return '<input type="password" name="%s" size="%s">'%(self._name, size) + + def confirm(self, size = 30): + ''' Render a second form edit field for the property, used for + confirmation that the user typed the password correctly. Generates + a field with name "name:confirm". + ''' + return '<input type="password" name="%s:confirm" size="%s">'%( + self._name, size) + +class NumberHTMLProperty(HTMLProperty): + def plain(self): + ''' Render a "plain" representation of the property + ''' + return str(self._value) + + def field(self, size = 30): + ''' Render a form edit field for the property + ''' + 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): + ''' Render a "plain" representation of the property + ''' + if self._value is None: + return '' + return self._value and "Yes" or "No" + + def field(self): + ''' Render a form edit field for the property + ''' + 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): + ''' Render a "plain" representation of the property + ''' + if self._value is None: + return '' + return str(self._value) + + def field(self, size = 30): + ''' Render a form edit field for the property + ''' + 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): + ''' Render the interval between the date and now. + + If the "pretty" flag is true, then make the display pretty. + ''' + if not self._value: + return '' + + # figure the interval + interval = date.Date('.') - self._value + if pretty: + return interval.pretty() + return str(interval) + + def pretty(self, format='%d %B %Y'): + ''' Render the date in a pretty format (eg. month names, spaces). + + The format string is a standard python strftime format string. + Note that if the day is zero, and appears at the start of the + string, then it'll be stripped from the output. This is handy + for the situatin when a date only specifies a month and a year. + ''' + return self._value.pretty() + + def local(self, offset): + ''' Return the date/time as a local (timezone offset) date/time. + ''' + return DateHTMLProperty(self._client, self._nodeid, self._prop, + self._name, self._value.local()) + +class IntervalHTMLProperty(HTMLProperty): + def plain(self): + ''' Render a "plain" representation of the property + ''' + if self._value is None: + return '' + return str(self._value) + + def pretty(self): + ''' Render the interval in a pretty format (eg. "yesterday") + ''' + return self._value.pretty() + + def field(self, size = 30): + ''' Render a form edit field for the property + ''' + 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 __init__(self, *args): + HTMLProperty.__init__(self, *args) + # if we're representing a form value, then the -1 from the form really + # should be a None + if str(self._value) == '-1': + self._value = None + + def __getattr__(self, attr): + ''' return a new HTMLItem ''' + #print 'Link.getattr', (self, attr, self._value) + if not self._value: + raise AttributeError, "Can't access missing value" + if self._prop.classname == 'user': + klass = HTMLUser + else: + klass = HTMLItem + i = klass(self._client, self._prop.classname, self._value) + return getattr(i, attr) + + def plain(self, escape=0): + ''' Render a "plain" representation of the property + ''' + if self._value is None: + return '' + linkcl = self._db.classes[self._prop.classname] + k = linkcl.labelprop(1) + value = str(linkcl.get(self._value, k)) + if escape: + value = cgi.escape(value) + return value + + def field(self, showid=0, size=None): + ''' Render a form edit field for the property + ''' + 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), (None, None)) + # TODO: make this a field display, not a menu one! + l = ['<select name="%s">'%self._name] + k = linkcl.labelprop(1) + if self._value is None: + s = 'selected ' + else: + s = '' + l.append(_('<option %svalue="-1">- no selection -</option>')%s) + + # make sure we list the current value if it's retired + if self._value and self._value not in options: + options.insert(0, self._value) + + for optionid in options: + # get the option value, and if it's None use an empty string + option = linkcl.get(optionid, k) or '' + + # figure if this option is selected + s = '' + if optionid == self._value: + s = 'selected ' + + # figure the label + if showid: + lab = '%s%s: %s'%(self._prop.classname, optionid, option) + else: + lab = option + + # truncate if it's too long + if size is not None and len(lab) > size: + lab = lab[:size-3] + '...' + + # and generate + lab = cgi.escape(lab) + l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab)) + l.append('</select>') + return '\n'.join(l) + + def menu(self, size=None, height=None, showid=0, additional=[], + **conditions): + ''' Render a form select list for this property + ''' + value = self._value + + # sort function + sortfunc = make_sort_function(self._db, self._prop.classname) + + 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, (None, None)) + + # make sure we list the current value if it's retired + if self._value and self._value not in options: + options.insert(0, self._value) + + for optionid in options: + # get the option value, and if it's None use an empty string + option = linkcl.get(optionid, k) or '' + + # figure if this option is selected + s = '' + if value in [optionid, option]: + s = 'selected ' + + # figure the label + if showid: + lab = '%s%s: %s'%(self._prop.classname, optionid, option) + else: + lab = option + + # truncate if it's too long + 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)) + + # and generate + 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 'Multi.getitem', (self, num) + value = self._value[num] + if self._prop.classname == 'user': + klass = HTMLUser + else: + klass = HTMLItem + return klass(self._client, self._prop.classname, value) + + def __contains__(self, value): + ''' Support the "in" operator. We have to make sure the passed-in + value is a string first, not a *HTMLProperty. + ''' + return str(value) in self._value + + def reverse(self): + ''' return the list in reverse order + ''' + l = self._value[:] + l.reverse() + if self._prop.classname == 'user': + klass = HTMLUser + else: + klass = HTMLItem + return [klass(self._client, self._prop.classname, value) for value in l] + + def plain(self, escape=0): + ''' Render a "plain" representation of the property + ''' + 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 + + def field(self, size=30, showid=0): + ''' Render a form edit field for the property + ''' + 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 linkcl.getkey(): + showid=1 + 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): + ''' Render a form select list for this property + ''' + 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, (None,None)) + height = height or min(len(options), 7) + l = ['<select multiple name="%s" size="%s">'%(self._name, height)] + k = linkcl.labelprop(1) + + # make sure we list the current values if they're retired + for val in value: + if val not in options: + options.insert(0, val) + + for optionid in options: + # get the option value, and if it's None use an empty string + option = linkcl.get(optionid, k) or '' + + # figure if this option is selected + s = '' + if optionid in value or option in value: + s = 'selected ' + + # figure the label + if showid: + lab = '%s%s: %s'%(self._prop.classname, optionid, option) + else: + lab = option + # truncate if it's too long + 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) + + # and generate + 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: + value = value.value.strip() + if not value: + return [] + return value.split(',') + +class ShowDict: + ''' A convenience access to the :columns index parameters + ''' + def __init__(self, columns): + self.columns = {} + for col in columns: + self.columns[col] = 1 + def __getitem__(self, name): + return self.columns.has_key(name) + +class HTMLRequest: + ''' The *request*, holding the CGI form and environment. + + "form" the CGI form as a cgi.FieldStorage + "env" the CGI environment variables + "base" the base URL for this instance + "user" a HTMLUser instance for this user + "classname" the current classname (possibly None) + "template" the current template (suffix, also possibly None) + + Index args: + "columns" dictionary of the columns to display in an index page + "show" a convenience access to columns - request/show/colname will + be true if the columns should be displayed, false otherwise + "sort" index sort column (direction, column name) + "group" index grouping property (direction, column name) + "filter" properties to filter the index on + "filterspec" values to filter the index on + "search_text" text to perform a full-text search on for an index + + ''' + 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, 'user', client.userid) + + # store the current class name and action + self.classname = client.classname + self.template = client.template + + self._post_init() + + def _post_init(self): + ''' Set attributes based on self.form + ''' + # extract the index display information from the form + self.columns = [] + if self.form.has_key(':columns'): + self.columns = handleListCGIValue(self.form[':columns']) + self.show = ShowDict(self.columns) + + # sorting + self.sort = (None, None) + if self.form.has_key(':sort'): + sort = self.form[':sort'].value + if sort.startswith('-'): + self.sort = ('-', sort[1:]) + else: + self.sort = ('+', sort) + if self.form.has_key(':sortdir'): + self.sort = ('-', self.sort[1]) + + # grouping + self.group = (None, None) + if self.form.has_key(':group'): + group = self.form[':group'].value + if group.startswith('-'): + self.group = ('-', group[1:]) + else: + self.group = ('+', group) + if self.form.has_key(':groupdir'): + self.group = ('-', self.group[1]) + + # filtering + self.filter = [] + if self.form.has_key(':filter'): + self.filter = handleListCGIValue(self.form[':filter']) + self.filterspec = {} + db = self.client.db + if self.classname is not None: + props = db.getclass(self.classname).getprops() + for name in self.filter: + if self.form.has_key(name): + prop = props[name] + fv = self.form[name] + if (isinstance(prop, hyperdb.Link) or + isinstance(prop, hyperdb.Multilink)): + self.filterspec[name] = lookupIds(db, prop, + handleListCGIValue(fv)) + else: + self.filterspec[name] = fv.value + + # full-text search argument + self.search_text = None + if self.form.has_key(':search_text'): + self.search_text = self.form[':search_text'].value + + # pagination - size and start index + # figure batch args + if self.form.has_key(':pagesize'): + self.pagesize = int(self.form[':pagesize'].value) + else: + self.pagesize = 50 + if self.form.has_key(':startwith'): + self.startwith = int(self.form[':startwith'].value) + else: + self.startwith = 0 + + def updateFromURL(self, url): + ''' Parse the URL for query args, and update my attributes using the + values. + ''' + self.form = {} + for name, value in cgi.parse_qsl(url): + if self.form.has_key(name): + if isinstance(self.form[name], type([])): + self.form[name].append(cgi.MiniFieldStorage(name, value)) + else: + self.form[name] = [self.form[name], + cgi.MiniFieldStorage(name, value)] + else: + self.form[name] = cgi.MiniFieldStorage(name, value) + self._post_init() + + def update(self, kwargs): + ''' Update my attributes using the keyword args + ''' + self.__dict__.update(kwargs) + if kwargs.has_key('columns'): + self.show = ShowDict(self.columns) + + def description(self): + ''' Return a description of the request - handle for the page title. + ''' + s = [self.client.db.config.TRACKER_NAME] + if self.classname: + if self.client.nodeid: + s.append('- %s%s'%(self.classname, self.client.nodeid)) + else: + if self.template == 'item': + s.append('- new %s'%self.classname) + elif self.template == 'index': + s.append('- %s index'%self.classname) + else: + s.append('- %s %s'%(self.classname, self.template)) + else: + s.append('- home') + return ' '.join(s) + + 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: %(template)r +columns: %(columns)r +sort: %(sort)r +group: %(group)r +filter: %(filter)r +search_text: %(search_text)r +pagesize: %(pagesize)r +startwith: %(startwith)r +env: %(env)s +'''%d + + def indexargs_form(self, columns=1, sort=1, group=1, filter=1, + filterspec=1): + ''' return the current index args as form elements ''' + l = [] + s = '<input type="hidden" name="%s" value="%s">' + if columns and self.columns: + l.append(s%(':columns', ','.join(self.columns))) + if sort and self.sort[1] is not None: + if self.sort[0] == '-': + val = '-'+self.sort[1] + else: + val = self.sort[1] + l.append(s%(':sort', val)) + if group and self.group[1] is not None: + if self.group[0] == '-': + val = '-'+self.group[1] + else: + val = self.group[1] + l.append(s%(':group', val)) + if filter and self.filter: + l.append(s%(':filter', ','.join(self.filter))) + if filterspec: + for k,v in self.filterspec.items(): + if type(v) == type([]): + l.append(s%(k, ','.join(v))) + else: + l.append(s%(k, v)) + if self.search_text: + l.append(s%(':search_text', self.search_text)) + l.append(s%(':pagesize', self.pagesize)) + l.append(s%(':startwith', self.startwith)) + return '\n'.join(l) + + def indexargs_url(self, url, args): + ''' embed the current index args in a URL ''' + l = ['%s=%s'%(k,v) for k,v in args.items()] + if self.columns and not args.has_key(':columns'): + l.append(':columns=%s'%(','.join(self.columns))) + if self.sort[1] is not None and not args.has_key(':sort'): + if self.sort[0] == '-': + val = '-'+self.sort[1] + else: + val = self.sort[1] + l.append(':sort=%s'%val) + if self.group[1] is not None and not args.has_key(':group'): + if self.group[0] == '-': + val = '-'+self.group[1] + else: + val = self.group[1] + l.append(':group=%s'%val) + if self.filter and not args.has_key(':columns'): + l.append(':filter=%s'%(','.join(self.filter))) + for k,v in self.filterspec.items(): + if not args.has_key(k): + if type(v) == type([]): + l.append('%s=%s'%(k, ','.join(v))) + else: + l.append('%s=%s'%(k, v)) + if self.search_text and not args.has_key(':search_text'): + l.append(':search_text=%s'%self.search_text) + if not args.has_key(':pagesize'): + l.append(':pagesize=%s'%self.pagesize) + if not args.has_key(':startwith'): + l.append(':startwith=%s'%self.startwith) + return '%s?%s'%(url, '&'.join(l)) + indexargs_href = indexargs_url + + 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) + if self.search_text: + matches = self.client.db.indexer.search( + re.findall(r'\b\w{2,25}\b', self.search_text), klass) + else: + matches = None + l = klass.filter(matches, filterspec, sort, group) + + # return the batch object, using IDs only + return Batch(self.client, l, self.pagesize, self.startwith, + classname=self.classname) + +# extend the standard ZTUtils Batch object to remove dependency on +# Acquisition and add a couple of useful methods +class Batch(ZTUtils.Batch): + ''' Use me to turn a list of items, or item ids of a given class, into a + series of batches. + + ========= ======================================================== + Parameter Usage + ========= ======================================================== + sequence a list of HTMLItems or item ids + classname if sequence is a list of ids, this is the class of item + size how big to make the sequence. + start where to start (0-indexed) in the sequence. + end where to end (0-indexed) in the sequence. + orphan if the next batch would contain less items than this + value, then it is combined with this batch + overlap the number of items shared between adjacent batches + ========= ======================================================== + + Attributes: Note that the "start" attribute, unlike the + argument, is a 1-based index (I know, lame). "first" is the + 0-based index. "length" is the actual number of elements in + the batch. + + "sequence_length" is the length of the original, unbatched, sequence. + ''' + def __init__(self, client, sequence, size, start, end=0, orphan=0, + overlap=0, classname=None): + self.client = client + self.last_index = self.last_item = None + self.current_item = None + self.classname = classname + self.sequence_length = len(sequence) + ZTUtils.Batch.__init__(self, sequence, 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 + + # move the last_item along - but only if the fetched index changes + # (for some reason, index 0 is fetched twice) + if index != self.last_index: + self.last_item = self.current_item + self.last_index = index + + item = self._sequence[index + self.first] + if self.classname: + # map the item ids to instances + if self.classname == 'user': + item = HTMLUser(self.client, self.classname, item) + else: + item = HTMLItem(self.client, self.classname, item) + self.current_item = item + return item + + def propchanged(self, property): + ''' Detect if the property marked as being the group property + changed in the last iteration fetch + ''' + if (self.last_item is None or + self.last_item[property] != self.current_item[property]): + return 1 + return 0 + + # override these 'cos we don't have access to acquisition + def previous(self): + if self.start == 1: + return None + return Batch(self.client, 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._sequence, self._size, + self.end - self.overlap, 0, self.orphan, self.overlap) + +class TemplatingUtils: + ''' Utilities for templating + ''' + def __init__(self, client): + self.client = client + def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0): + return Batch(self.client, sequence, size, start, end, orphan, + overlap) +
