X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fcgi%2Ftemplating.py;h=d29f31d6600aac87eae1d8eae92f55a511c90a2b;hb=ef558f6cdc27a8289fffae910e873ccf3ef1bfcd;hp=a01fa7c768b2740d13e176a2e2eb12b0e22989f2;hpb=a00a40e19fdbd91e869a7dc70ff809baf1d5b99f;p=roundup.git diff --git a/roundup/cgi/templating.py b/roundup/cgi/templating.py index a01fa7c..d29f31d 100644 --- a/roundup/cgi/templating.py +++ b/roundup/cgi/templating.py @@ -1,37 +1,106 @@ -import sys, cgi, urllib, os +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 -# 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 +# 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 + +# XXX WAH pagetemplates aren't pickleable :( +#def getTemplate(dir, name, classname=None, request=None): +# ''' Interface to get a template, possibly loading a compiled template. +# ''' +# # source +# src = os.path.join(dir, name) +# +# # see if we can get a compile from the template"c" directory (most +# # likely is "htmlc" +# split = list(os.path.split(dir)) +# split[-1] = split[-1] + 'c' +# cdir = os.path.join(*split) +# split.append(name) +# cpl = os.path.join(*split) +# +# # ok, now see if the source is newer than the compiled (or if the +# # compiled even exists) +# MTIME = os.path.stat.ST_MTIME +# if (not os.path.exists(cpl) or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]): +# # nope, we need to compile +# pt = RoundupPageTemplate() +# pt.write(open(src).read()) +# pt.id = name +# +# # save off the compiled template +# if not os.path.exists(cdir): +# os.makedirs(cdir) +# f = open(cpl, 'wb') +# pickle.dump(pt, f) +# f.close() +# else: +# # yay, use the compiled template +# f = open(cpl, 'rb') +# pt = pickle.load(f) +# return pt + +templates = {} + +def getTemplate(dir, name, extension, classname=None, request=None): + ''' 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(dir, filename) + try: + stime = os.stat(src)[os.path.stat.ST_MTIME] + except os.error, error: + if error.errno != errno.ENOENT or not extension: + raise + # try for a generic template + filename = '_generic.%s'%extension + src = os.path.join(dir, filename) + stime = os.stat(src)[os.path.stat.ST_MTIME] + + key = (dir, filename) + if templates.has_key(key) and stime < templates[key].mtime: + # compiled template is up to date + return templates[key] + + # compile the template + templates[key] = pt = RoundupPageTemplate() + pt.write(open(src).read()) + pt.id = filename + pt.mtime = time.time() + return pt class RoundupPageTemplate(PageTemplate.PageTemplate): ''' A Roundup-specific PageTemplate. @@ -77,36 +146,46 @@ class RoundupPageTemplate(PageTemplate.PageTemplate): 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): + def getContext(self, client, classname, request): c = { - 'klass': HTMLClass(self.client, self.classname), + 'klass': HTMLClass(client, classname), 'options': {}, 'nothing': None, - 'request': self.request, - 'content': self.client.content, - 'db': HTMLDatabase(self.client), - 'instance': self.client.instance + 'request': request, + 'content': client.content, + 'db': HTMLDatabase(client), + 'instance': 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'] + if client.nodeid: + c['item'] = HTMLItem(client.db, classname, client.nodeid) + c[classname] = c['item'] else: - c[self.classname] = c['klass'] + c[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}) + + 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 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._v_macros, + getEngine().getContext(c), output, tal=1, strictinsert=0)() + return output.getvalue() class HTMLDatabase: ''' Return HTMLClasses for valid class fetches @@ -115,7 +194,10 @@ class HTMLDatabase: self.client = client self.config = client.db.config def __getattr__(self, attr): - self.client.db.getclass(attr) + try: + self.client.db.getclass(attr) + except KeyError: + raise AttributeError, attr return HTMLClass(self.client, attr) def classes(self): l = self.client.db.classes.keys() @@ -136,15 +218,15 @@ class HTMLClass: def __repr__(self): return ''%(id(self), self.classname) - def __getattr__(self, attr): + def __getitem__(self, item): ''' return an HTMLItem instance''' - #print 'getattr', (self, attr) - if attr == 'creator': + #print 'getitem', (self, attr) + if item == 'creator': return HTMLUser(self.client) - if not self.props.has_key(attr): - raise AttributeError, attr - prop = self.props[attr] + if not self.props.has_key(item): + raise KeyError, item + prop = self.props[item] # look up the correct HTMLProperty class for klass, htmlklass in propclasses: @@ -153,10 +235,17 @@ class HTMLClass: else: value = None if isinstance(prop, klass): - return htmlklass(self.db, '', prop, attr, value) + return htmlklass(self.db, '', prop, item, value) # no good - raise AttributeError, attr + raise KeyError, item + + def __getattr__(self, attr): + ''' convenience access ''' + try: + return self[attr] + except KeyError: + raise AttributeError, attr def properties(self): ''' Return HTMLProperty for all props @@ -176,6 +265,40 @@ class HTMLClass: l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()] 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 @@ -199,9 +322,9 @@ class HTMLClass: 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) + return ''\ + '(%s)'%(self.classname, properties, width, height, label) def submit(self, label="Submit New Entry"): ''' Generate a submit button (and action hidden element) @@ -218,19 +341,15 @@ class HTMLClass: # create a new request and override the specified args req = HTMLRequest(self.client) req.classname = self.classname - req.__dict__.update(kwargs) + req.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 + pt = getTemplate(self.db.config.TEMPLATES, self.classname, name) # XXX handle PT rendering errors here nicely try: - return pt.render() + # use our fabricated request + return pt.render(self.client, self.classname, req) except PageTemplate.PTRuntimeError, message: return '%s
    %s
'%(message, cgi.escape('
  • '.join(pt._v_errors))) @@ -248,29 +367,33 @@ class HTMLItem: def __repr__(self): return ''%(id(self), self.classname, self.nodeid) - def __getattr__(self, attr): + def __getitem__(self, item): ''' return an HTMLItem instance''' - #print 'getattr', (self, attr) - if attr == 'id': + if item == 'id': return self.nodeid - - if not self.props.has_key(attr): - raise AttributeError, attr - prop = self.props[attr] + if not self.props.has_key(item): + raise KeyError, item + prop = self.props[item] # get the value, handling missing values - value = self.klass.get(self.nodeid, attr, None) + value = self.klass.get(self.nodeid, item, None) if value is None: - if isinstance(self.props[attr], hyperdb.Multilink): + 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.db, self.nodeid, prop, attr, value) + return htmlklass(self.db, self.nodeid, prop, item, value) - # no good - raise AttributeError, attr + 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) @@ -613,7 +736,7 @@ class LinkHTMLProperty(HTMLProperty): def plain(self, escape=0): if self.value is None: return _('[unselected]') - linkcl = self.db.classes[self.klass.classname] + linkcl = self.db.classes[self.prop.classname] k = linkcl.labelprop(1) value = str(linkcl.get(self.value, k)) if escape: @@ -688,10 +811,10 @@ class LinkHTMLProperty(HTMLProperty): s = 'selected ' l.append(_('')%s) if linkcl.getprops().has_key('order'): - sort_on = 'order' + sort_on = ('+', 'order') else: - sort_on = linkcl.labelprop() - options = linkcl.filter(None, conditions, [sort_on], []) + sort_on = ('+', linkcl.labelprop()) + options = linkcl.filter(None, conditions, sort_on, (None, None)) for optionid in options: option = linkcl.get(optionid, k) s = '' @@ -735,6 +858,12 @@ class MultilinkHTMLProperty(HTMLProperty): value = self.value[num] return HTMLItem(self.db, self.prop.classname, value) + def reverse(self): + ''' return the list in reverse order ''' + l = self.value[:] + l.reverse() + return [HTMLItem(self.db, self.prop.classname, value) for value in l] + def plain(self, escape=0): linkcl = self.db.classes[self.prop.classname] k = linkcl.labelprop(1) @@ -772,10 +901,10 @@ class MultilinkHTMLProperty(HTMLProperty): linkcl = self.db.getclass(self.prop.classname) if linkcl.getprops().has_key('order'): - sort_on = 'order' + sort_on = ('+', 'order') else: - sort_on = linkcl.labelprop() - options = linkcl.filter(None, conditions, [sort_on], []) + sort_on = ('+', linkcl.labelprop()) + options = linkcl.filter(None, conditions, sort_on, (None,None)) height = height or min(len(options), 7) l = ['' - 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: + 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))) - for k,v in self.filterspec.items(): - l.append(s%(k, ','.join(v))) + if filterspec: + for k,v in self.filterspec.items(): + l.append(s%(k, ','.join(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_href(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: - 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: + 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(): - l.append('%s=%s'%(k, ','.join(v))) + if not args.has_key(k): + l.append('%s=%s'%(k, ','.join(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)) def base_javascript(self): @@ -954,25 +1203,26 @@ function help_window(helpurl, width, height) { # 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) + if self.search_text: + matches = self.client.db.indexer.search( + re.findall(r'\b\w{2,25}\b', self.search_text), klass) else: - start = 0 + matches = None + l = klass.filter(matches, filterspec, sort, group) # return the batch object - return Batch(self.client, self.classname, l, size, start) + return Batch(self.client, self.classname, l, self.pagesize, + self.startwith) + +# extend the standard ZTUtils Batch object to remove dependency on +# Acquisition and add a couple of useful methods class Batch(ZTUtils.Batch): def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0): self.client = client self.classname = classname + self.last_index = self.last_item = None + self.current_item = None ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap) # overwrite so we can late-instantiate the HTMLItem instance @@ -983,13 +1233,28 @@ class Batch(ZTUtils.Batch): 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 + # wrap the return in an HTMLItem - return HTMLItem(self.client.db, self.classname, + self.current_item = HTMLItem(self.client.db, self.classname, self._sequence[index+self.first]) + return self.current_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): - print self.start if self.start == 1: return None return Batch(self.client, self.classname, self._sequence, self._size,