index a01fa7c768b2740d13e176a2e2eb12b0e22989f2..d29f31d6600aac87eae1d8eae92f55a511c90a2b 100644 (file)
-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.
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
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()
def __repr__(self):
return '<HTMLClass(0x%x) %s>'%(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:
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
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
You may optionally override the label displayed, the width and
height. The popup window will be resizable and scrollable.
'''
- return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
- 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(self.classname,
- properties, width, height, label)
+ return '<a href="javascript:help_window(\'%s?:template=help&' \
+ ':contentonly=1&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)
# 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 '<strong>%s</strong><ol>%s</ol>'%(message,
cgi.escape('<li>'.join(pt._v_errors)))
def __repr__(self):
return '<HTMLItem(0x%x) %s %s>'%(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)
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:
s = 'selected '
l.append(_('<option %svalue="-1">- no selection -</option>')%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 = ''
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)
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 = ['<select multiple name="%s" size="%s">'%(self.name, height)]
k = linkcl.labelprop(1)
else:
return value.value.split(',')
-# XXX This is starting to look a lot (in data terms) like the client object
-# itself!
+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
+ "url" the current URL path for this request
+ "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
self.form = client.form
self.env = client.env
self.base = client.base
+ self.url = client.url
self.user = HTMLUser(client)
# store the current class name and action
self.classname = client.classname
- self.template_type = client.template_type
+ self.template = client.template
# extract the index display information from the form
- self.columns = {}
+ self.columns = []
if self.form.has_key(':columns'):
- for entry in handleListCGIValue(self.form[':columns']):
- self.columns[entry] = 1
- self.sort = []
+ self.columns = handleListCGIValue(self.form[':columns'])
+ self.show = ShowDict(self.columns)
+
+ # sorting
+ self.sort = (None, None)
if self.form.has_key(':sort'):
- self.sort = handleListCGIValue(self.form[':sort'])
- self.group = []
+ 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'):
- self.group = handleListCGIValue(self.form[':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 = {}
- for name in self.filter:
- if self.form.has_key(name):
- self.filterspec[name]=handleListCGIValue(self.form[name])
+ if self.classname is not None:
+ props = self.client.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] = 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 update(self, kwargs):
+ 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.INSTANCE_NAME]
+ if self.classname:
+ if self.client.nodeid:
+ s.append('- %s%s'%(self.classname, self.client.nodeid))
+ else:
+ s.append('- index of '+self.classname)
+ else:
+ s.append('- home')
+ return ' '.join(s)
def __str__(self):
d = {}
d['env'] = e
return '''
form: %(form)s
+url: %(url)r
base: %(base)r
classname: %(classname)r
-template_type: %(template_type)r
+template: %(template)r
columns: %(columns)r
sort: %(sort)r
group: %(group)r
filter: %(filter)r
-filterspec: %(filterspec)r
+search_text: %(search_text)r
+pagesize: %(pagesize)r
+startwith: %(startwith)r
env: %(env)s
'''%d
- def indexargs_form(self):
+ 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 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):
# 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
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,