index 2a04f5ab44ea1eba69522dccd351f10bd4587ae7..2f2a478f14e570dbe49dc0cae1c0b21f67dfb6a5 100644 (file)
-import sys, cgi, urllib, os, re
+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 = {}
+
+class NoTemplate(Exception):
+ pass
+
+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:
+ 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(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
+
+ 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.
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
+ *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
``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
*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):
+ def getContext(self, client, classname, request):
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
+ 'request': request,
+ 'content': client.content,
+ 'db': HTMLDatabase(client),
+ 'instance': client.instance,
+ 'utils': TemplatingUtils(client),
}
# 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:
+ if classname == 'user':
+ c['context'] = HTMLUser(client, classname, client.nodeid)
+ else:
+ c['context'] = HTMLItem(client, classname, client.nodeid)
else:
- c[self.classname] = c['klass']
+ c['context'] = HTMLClass(client, classname)
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 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._v_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
+ 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):
- self.client.db.getclass(attr)
- return HTMLClass(self.client, attr)
+ try:
+ return self[attr]
+ except KeyError:
+ raise AttributeError, attr
+
def classes(self):
- l = self.client.db.classes.keys()
+ l = self._client.db.classes.keys()
l.sort()
- return [HTMLClass(self.client, cn) for cn in l]
-
-class HTMLClass:
+ 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:
+ l.append(cl.lookup(entry))
+ 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
- self.classname = 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
if classname is not None:
- self.klass = self.db.getclass(self.classname)
- self.props = self.klass.getprops()
+ 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 HTMLItem instance'''
- #print 'getitem', (self, attr)
- if item == 'creator':
- return HTMLUser(self.client)
+ ''' return an HTMLProperty instance
+ '''
+ #print 'HTMLClass.getitem', (self, item)
+
+ # we don't exist
+ if item == 'id':
+ return None
- if not self.props.has_key(item):
- raise KeyError, item
- prop = self.props[item]
+ # get the property
+ prop = self._props[item]
# look up the correct HTMLProperty class
+ form = self._client.form
for klass, htmlklass in propclasses:
- if isinstance(prop, hyperdb.Multilink):
- value = []
+ 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:
- value = None
- if isinstance(prop, klass):
- return htmlklass(self.db, '', prop, item, value)
+ if isinstance(prop, hyperdb.Multilink):
+ value = []
+ else:
+ value = None
+ return htmlklass(self._client, '', prop, item, value)
# no good
raise KeyError, item
raise AttributeError, attr
def properties(self):
- ''' Return HTMLProperty for all props
+ ''' Return HTMLProperty for all of this class' properties.
'''
l = []
- for name, prop in self.props.items():
+ 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))
+ l.append(htmlklass(self._client, '', prop, name, value))
return l
def list(self):
- l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()]
+ ''' 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._prop.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
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)]
+ 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, label='?', width='400', height='400'):
- '''pop up a javascript window with class help
+ 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').
+ 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.
+ 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)
+ 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&' \
+ ':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)
''' Render this class with the given template.
'''
# create a new request and override the specified args
- req = HTMLRequest(self.client)
+ 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)
+ pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
- # use the specified template
- name = self.classname + '.' + name
- pt.write(open('/tmp/test/html/%s'%name).read())
- pt.id = name
-
- # XXX handle PT rendering errors here nicely
- try:
- return pt.render()
- except PageTemplate.PTRuntimeError, message:
- return '<strong>%s</strong><ol>%s</ol>'%(message,
- cgi.escape('<li>'.join(pt._v_errors)))
+ # use our fabricated request
+ return pt.render(self._client, self.classname, req)
-class HTMLItem:
+class HTMLItem(HTMLPermissions):
''' 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 __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)
+ return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
+ self._nodeid)
def __getitem__(self, item):
- ''' return an HTMLItem instance'''
+ ''' return an HTMLProperty instance
+ '''
+ #print 'HTMLItem.getitem', (self, item)
if item == 'id':
- return self.nodeid
- if not self.props.has_key(item):
- raise KeyError, item
- prop = self.props[item]
+ return self._nodeid
+
+ # get the property
+ prop = self._props[item]
# get the value, handling missing values
- value = self.klass.get(self.nodeid, item, None)
+ value = self._klass.get(self._nodeid, item, None)
if value is None:
- if isinstance(self.props[item], 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, item, value)
+ return htmlklass(self._client, self._nodeid, prop, item, value)
raise KeyErorr, item
return ' <input type="hidden" name=":action" value="edit">\n'\
' <input type="submit" name="submit" value="%s">'%label
- # XXX this probably should just return the history items, not the HTML
+ def journal(self, direction='descending'):
+ ''' Return a list of HTMLJournalEntry instances.
+ '''
+ # XXX do this
+ return []
+
def history(self, direction='descending'):
- l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
- '<tr class="list-header">',
- _('<th align=left><span class="list-item">Date</span></th>'),
- _('<th align=left><span class="list-item">User</span></th>'),
- _('<th align=left><span class="list-item">Action</span></th>'),
- _('<th align=left><span class="list-item">Args</span></th>'),
+ 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 = self._klass.history(self._nodeid)
history.sort()
if direction == 'descending':
history.reverse()
# try to get the relevant property and treat it
# specially
try:
- prop = self.props[k]
+ prop = self._props[k]
except KeyError:
prop = None
if prop is not None:
# figure what the link class is
classname = prop.classname
try:
- linkcl = self.db.getclass(classname)
+ 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,
+ os.path.join(self._db.config.TEMPLATES,
classname+'.item'))
if isinstance(prop, hyperdb.Multilink) and \
handled by the history display!</em></strong>''')
arg_s = '<strong><em>' + str(args) + '</em></strong>'
date_s = date_s.replace(' ', ' ')
- l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
- '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
- user, action, arg_s))
+ 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('</table>')
return '\n'.join(l)
- def remove(self):
- # XXX do what?
- return ''
+ 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')
+ req.updateFromURL(self._klass.get(self._nodeid, 'url'))
+
+ # new template, using the specified classname and request
+ pt = getTemplate(self._db.config.TEMPLATES, 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):
- HTMLItem.__init__(self, client.db, 'user', client.userid)
- self.default_classname = client.classname
- self.userid = client.userid
+ 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
+ self._security = client.db.security
+
_marker = []
def hasPermission(self, role, classname=_marker):
''' Determine if the user has the Role.
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)
+ 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, db, nodeid, prop, name, value):
- self.db = db
- self.nodeid = nodeid
- self.prop = prop
- self.name = name
- self.value = value
+ 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)
+ 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)
+ return cmp(self._value, other._value)
+ return cmp(self._value, other)
class StringHTMLProperty(HTMLProperty):
def plain(self, escape=0):
- if self.value is None:
+ ''' Render a "plain" representation of the property
+ '''
+ if self._value is None:
return ''
if escape:
- return cgi.escape(str(self.value))
- return str(self.value)
+ return cgi.escape(str(self._value))
+ return str(self._value)
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):
- if self.value is None:
+ ''' Render a form edit field for the property
+ '''
+ if self._value is None:
value = ''
else:
- value = cgi.escape(str(self.value))
+ value = cgi.escape(str(self._value))
value = '"'.join(value.split('"'))
- return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
+ return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
def multiline(self, escape=0, rows=5, cols=40):
- if self.value is None:
+ ''' Render a multiline form edit field for the property
+ '''
+ if self._value is None:
value = ''
else:
- value = cgi.escape(str(self.value))
+ 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)
+ 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('.', ' ')
+ ''' 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):
- if self.value is None:
+ ''' Render a "plain" representation of the property
+ '''
+ if self._value is None:
return ''
return _('*encrypted*')
def field(self, size = 30):
- return '<input type="password" name="%s" size="%s">'%(self.name, size)
+ ''' 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):
- return str(self.value)
+ ''' Render a "plain" representation of the property
+ '''
+ return str(self._value)
def field(self, size = 30):
- if self.value is None:
+ ''' Render a form edit field for the property
+ '''
+ if self._value is None:
value = ''
else:
- value = cgi.escape(str(self.value))
+ value = cgi.escape(str(self._value))
value = '"'.join(value.split('"'))
- return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
+ 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"
+ return self._value and "Yes" or "No"
def field(self):
- checked = self.value and "checked" or ""
- s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self.name,
+ ''' 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,
+ s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
checked)
return s
class DateHTMLProperty(HTMLProperty):
def plain(self):
- if self.value is None:
+ ''' Render a "plain" representation of the property
+ '''
+ if self._value is None:
return ''
- return str(self.value)
+ return str(self._value)
def field(self, size = 30):
- if self.value is None:
+ ''' Render a form edit field for the property
+ '''
+ if self._value is None:
value = ''
else:
- value = cgi.escape(str(self.value))
+ value = cgi.escape(str(self._value))
value = '"'.join(value.split('"'))
- return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
+ return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
def reldate(self, pretty=1):
- if not self.value:
+ ''' 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
+ interval = date.Date('.') - self._value
if pretty:
return interval.pretty()
return str(interval)
class IntervalHTMLProperty(HTMLProperty):
def plain(self):
- if self.value is None:
+ ''' Render a "plain" representation of the property
+ '''
+ if self._value is None:
return ''
- return str(self.value)
+ return str(self._value)
def pretty(self):
- return self.value.pretty()
+ ''' Render the interval in a pretty format (eg. "yesterday")
+ '''
+ return self._value.pretty()
def field(self, size = 30):
- if self.value is None:
+ ''' Render a form edit field for the property
+ '''
+ if self._value is None:
value = ''
else:
- value = cgi.escape(str(self.value))
+ value = cgi.escape(str(self._value))
value = '"'.join(value.split('"'))
- return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
+ return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
class LinkHTMLProperty(HTMLProperty):
''' Link HTMLProperty
'''
def __getattr__(self, attr):
''' return a new HTMLItem '''
- #print 'getattr', (self, attr, self.value)
- if not self.value:
+ #print 'Link.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)
+ 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):
- if self.value is None:
- return _('[unselected]')
- linkcl = self.db.classes[self.prop.classname]
+ ''' 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))
+ 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)
+ 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], [])
+ options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
# TODO: make this a field display, not a menu one!
- l = ['<select name="%s">'%property]
+ l = ['<select name="%s">'%self._name]
k = linkcl.labelprop(1)
- if value is None:
+ if self._value is None:
s = 'selected '
else:
s = ''
for optionid in options:
option = linkcl.get(optionid, k)
s = ''
- if optionid == value:
+ if optionid == self._value:
s = 'selected '
if showid:
- lab = '%s%s: %s'%(self.prop.classname, optionid, option)
+ lab = '%s%s: %s'%(self._prop.classname, optionid, option)
else:
lab = option
if size is not None and len(lab) > size:
l.append('</select>')
return '\n'.join(l)
- def download(self, showid=0):
- linkname = self.prop.classname
- linkcl = self.db.getclass(linkname)
- k = linkcl.labelprop(1)
- linkvalue = cgi.escape(str(linkcl.get(self.value, k)))
- if showid:
- label = value
- title = ' title="%s"'%linkvalue
- # note ... this should be urllib.quote(linkcl.get(value, k))
- else:
- label = linkvalue
- title = ''
- return '<a href="%s%s/%s"%s>%s</a>'%(linkname, self.value,
- linkvalue, title, label)
-
def menu(self, size=None, height=None, showid=0, additional=[],
**conditions):
- value = self.value
+ ''' Render a form select list for this property
+ '''
+ value = self._value
# sort function
- sortfunc = make_sort_function(self.db, self.prop.classname)
+ sortfunc = make_sort_function(self._db, self._prop.classname)
- # force the value to be a single choice
- if isinstance(value, type('')):
- value = value[0]
- linkcl = self.db.getclass(self.prop.classname)
- l = ['<select name="%s">'%self.name]
+ linkcl = self._db.getclass(self._prop.classname)
+ l = ['<select name="%s">'%self._name]
k = linkcl.labelprop(1)
s = ''
if value is None:
if value in [optionid, option]:
s = 'selected '
if showid:
- lab = '%s%s: %s'%(self.prop.classname, optionid, option)
+ lab = '%s%s: %s'%(self._prop.classname, optionid, option)
else:
lab = option
if size is not None and len(lab) > size:
l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
l.append('</select>')
return '\n'.join(l)
-
# def checklist(self, ...)
class MultilinkHTMLProperty(HTMLProperty):
'''
def __len__(self):
''' length of the multilink '''
- return len(self.value)
+ 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)
+ ''' 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
+ '''
+ return value in self._value
def reverse(self):
- ''' return the list in reverse order '''
- l = self.value[:]
+ ''' return the list in reverse order
+ '''
+ l = self._value[:]
l.reverse()
- return [HTMLItem(self.db, self.prop.classname, value) for value in l]
+ 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):
- linkcl = self.db.classes[self.prop.classname]
+ ''' Render a "plain" representation of the property
+ '''
+ linkcl = self._db.classes[self._prop.classname]
k = linkcl.labelprop(1)
labels = []
- for v in self.value:
+ 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[:]
+ ''' 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)
+ return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
def menu(self, size=None, height=None, showid=0, additional=[],
**conditions):
- value = self.value
+ ''' Render a form select list for this property
+ '''
+ value = self._value
# sort function
- sortfunc = make_sort_function(self.db, self.prop.classname)
+ sortfunc = make_sort_function(self._db, self._prop.classname)
- linkcl = self.db.getclass(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)]
+ l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
k = linkcl.labelprop(1)
for optionid in options:
option = linkcl.get(optionid, k)
if optionid in value or option in value:
s = 'selected '
if showid:
- lab = '%s%s: %s'%(self.prop.classname, optionid, option)
+ lab = '%s%s: %s'%(self._prop.classname, optionid, option)
else:
lab = option
if size is not None and len(lab) > size:
if isinstance(value, type([])):
return [value.value for value in value]
else:
- return value.value.split(',')
+ 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)
-# 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.
+ "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
self.form = client.form
self.env = client.env
self.base = client.base
- self.user = HTMLUser(client)
+ self.user = HTMLUser(client, 'user', client.userid)
# store the current class name and action
self.classname = client.classname
- self.template_type = client.template_type
+ 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 = {}
+ self.columns = []
if self.form.has_key(':columns'):
- for entry in handleListCGIValue(self.form[':columns']):
- self.columns[entry] = 1
+ self.columns = handleListCGIValue(self.form[':columns'])
+ self.show = ShowDict(self.columns)
# sorting
self.sort = (None, None)
if self.form.has_key(':filter'):
self.filter = handleListCGIValue(self.form[':filter'])
self.filterspec = {}
- props = self.client.db.getclass(self.classname).getprops()
- for name in self.filter:
- if self.form.has_key(name):
- prop = props[name]
- if (isinstance(prop, hyperdb.Link) or
- isinstance(prop, hyperdb.Multilink)):
- self.filterspec[name] = handleListCGIValue(self.form[name])
- else:
- self.filterspec[name] = self.form[name].value
+ 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 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__)
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
l = []
s = '<input type="hidden" name="%s" value="%s">'
if columns and self.columns:
- l.append(s%(':columns', ','.join(self.columns.keys())))
- if sort and self.sort is not None:
- l.append(s%(':sort', self.sort))
- if group and self.group is not None:
- l.append(s%(':group', self.group))
+ 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():
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):
+ 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:
- l.append(':columns=%s'%(','.join(self.columns.keys())))
- if self.sort is not None:
- l.append(':sort=%s'%self.sort)
- if self.group is not None:
- l.append(':group=%s'%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))
+ indexargs_href = indexargs_url
def base_javascript(self):
return '''
matches = None
l = klass.filter(matches, 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)
+ # map the item ids to instances
+ if self.classname == 'user':
+ klass = HTMLUser
else:
- start = 0
+ klass = HTMLItem
+ l = [klass(self.client, self.classname, item) for item in l]
# return the batch object
- return Batch(self.client, self.classname, l, size, start)
+ return Batch(self.client, 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):
+ ''' 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
+ 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):
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)
+ 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 + self.end < self.first: raise IndexError, index
return self._sequence[index + self.end]
- if index >= self.length: raise IndexError, index
+ 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)
self.last_item = self.current_item
self.last_index = index
- # wrap the return in an HTMLItem
- self.current_item = HTMLItem(self.client.db, self.classname,
- self._sequence[index+self.first])
+ self.current_item = self._sequence[index + self.first]
return self.current_item
def propchanged(self, property):
def previous(self):
if self.start == 1:
return None
- return Batch(self.client, self.classname, self._sequence, self._size,
+ return Batch(self.client, self._sequence, self._size,
self.first - self._size + self.overlap, 0, self.orphan,
self.overlap)
self._sequence[self.end]
except IndexError:
return None
- return Batch(self.client, self.classname, self._sequence, self._size,
+ return Batch(self.client, 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
+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)