X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fhtmltemplate.py;h=2f6557a1fdb0abba2b3e4d660048c9cfdc2e923b;hb=f04ca7e8c6067e05296508ed2b7c302425ffbcc8;hp=3d9d69e70b3933f21ab964327230d44135fe302f;hpb=a4c361720db112dc077d635014985213ef77155f;p=roundup.git diff --git a/roundup/htmltemplate.py b/roundup/htmltemplate.py index 3d9d69e..2f6557a 100644 --- a/roundup/htmltemplate.py +++ b/roundup/htmltemplate.py @@ -15,37 +15,76 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: htmltemplate.py,v 1.21 2001-08-16 07:34:59 richard Exp $ +# $Id: htmltemplate.py,v 1.89 2002-05-15 06:34:47 richard Exp $ -import os, re, StringIO, urllib, cgi, errno +__doc__ = """ +Template engine. +""" -import hyperdb, date +import os, re, StringIO, urllib, cgi, errno, types, urllib -class Base: - def __init__(self, db, templates, classname, nodeid=None, form=None, - filterspec=None): - # TODO: really not happy with the way templates is passed on here - self.db, self.templates = db, templates - self.classname, self.nodeid = classname, nodeid - self.form, self.filterspec = form, filterspec - self.cl = self.db.classes[self.classname] - self.properties = self.cl.getprops() +import hyperdb, date +from i18n import _ -class Plain(Base): - ''' display a String property directly; +# This imports the StructureText functionality for the do_stext function +# get it from http://dev.zope.org/Members/jim/StructuredTextWiki/NGReleases +try: + from StructuredText.StructuredText import HTML as StructuredText +except ImportError: + StructuredText = None - display a Date property in a specified time zone with an option to - omit the time from the date stamp; +class MissingTemplateError(ValueError): + '''Error raised when a template file is missing + ''' + pass - for a Link or Multilink property, display the key strings of the - linked nodes (or the ids if the linked class has no key property) +class TemplateFunctions: + '''Defines the templating functions that are used in the HTML templates + of the roundup web interface. ''' - def __call__(self, property): + def __init__(self): + self.form = None + self.nodeid = None + self.filterspec = None + self.globals = {} + for key in TemplateFunctions.__dict__.keys(): + if key[:3] == 'do_': + self.globals[key[3:]] = getattr(self, key) + + # These are added by the subclass where appropriate + self.client = None + self.instance = None + self.templates = None + self.classname = None + self.db = None + self.cl = None + self.properties = None + + def do_plain(self, property, escape=0): + ''' display a String property directly; + + display a Date property in a specified time zone with an option to + omit the time from the date stamp; + + for a Link or Multilink property, display the key strings of the + linked nodes (or the ids if the linked class has no key property) + ''' if not self.nodeid and self.form is None: - return '[Field: not called from item]' + return _('[Field: not called from item]') propclass = self.properties[property] if self.nodeid: - value = self.cl.get(self.nodeid, property) + # make sure the property is a valid one + # TODO: this tests, but we should handle the exception + dummy = self.cl.getprops()[property] + + # get the value for this property + try: + value = self.cl.get(self.nodeid, property) + except KeyError: + # a KeyError here means that the node doesn't have a value + # for the specified property + if isinstance(propclass, hyperdb.Multilink): value = [] + else: value = '' else: # TODO: pull the value from the form if isinstance(propclass, hyperdb.Multilink): value = [] @@ -53,62 +92,120 @@ class Plain(Base): if isinstance(propclass, hyperdb.String): if value is None: value = '' else: value = str(value) + elif isinstance(propclass, hyperdb.Password): + if value is None: value = '' + else: value = _('*encrypted*') elif isinstance(propclass, hyperdb.Date): + # this gives "2002-01-17.06:54:39", maybe replace the "." by a " ". value = str(value) elif isinstance(propclass, hyperdb.Interval): value = str(value) elif isinstance(propclass, hyperdb.Link): linkcl = self.db.classes[propclass.classname] k = linkcl.labelprop() - if value: value = str(linkcl.get(value, k)) - else: value = '[unselected]' + if value: + value = linkcl.get(value, k) + else: + value = _('[unselected]') elif isinstance(propclass, hyperdb.Multilink): linkcl = self.db.classes[propclass.classname] k = linkcl.labelprop() - value = ', '.join([linkcl.get(i, k) for i in value]) + labels = [] + for v in value: + labels.append(linkcl.get(v, k)) + value = ', '.join(labels) else: - s = 'Plain: bad propclass "%s"'%propclass + value = _('Plain: bad propclass "%(propclass)s"')%locals() + if escape: + value = cgi.escape(value) return value -class Field(Base): - ''' display a property like the plain displayer, but in a text field - to be edited - ''' - def __call__(self, property, size=None, height=None, showid=0): - if not self.nodeid and self.form is None and self.filterspec is None: - return '[Field: not called from item]' + def do_stext(self, property, escape=0): + '''Render as structured text using the StructuredText module + (see above for details) + ''' + s = self.do_plain(property, escape=escape) + if not StructuredText: + return s + return StructuredText(s,level=1,header=0) + + def determine_value(self, property): + '''determine the value of a property using the node, form or + filterspec + ''' propclass = self.properties[property] if self.nodeid: value = self.cl.get(self.nodeid, property, None) - # TODO: remove this from the code ... it's only here for - # handling schema changes, and they should be handled outside - # of this code... if isinstance(propclass, hyperdb.Multilink) and value is None: - value = [] + return [] + return value elif self.filterspec is not None: if isinstance(propclass, hyperdb.Multilink): - value = self.filterspec.get(property, []) + return self.filterspec.get(property, []) else: - value = self.filterspec.get(property, '') + return self.filterspec.get(property, '') + # TODO: pull the value from the form + if isinstance(propclass, hyperdb.Multilink): + return [] else: - # TODO: pull the value from the form - if isinstance(propclass, hyperdb.Multilink): value = [] - else: value = '' + return '' + + def make_sort_function(self, classname): + '''Make a sort function for a given class + ''' + linkcl = self.db.classes[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 do_field(self, property, size=None, showid=0): + ''' display a property like the plain displayer, but in a text field + to be edited + + Note: if you would prefer an option list style display for + link or multilink editing, use menu(). + ''' + if not self.nodeid and self.form is None and self.filterspec is None: + return _('[Field: not called from item]') + + if size is None: + size = 30 + + propclass = self.properties[property] + + # get the value + value = self.determine_value(property) + + # now display if (isinstance(propclass, hyperdb.String) or isinstance(propclass, hyperdb.Date) or isinstance(propclass, hyperdb.Interval)): - size = size or 30 if value is None: value = '' else: - value = cgi.escape(value) + value = cgi.escape(str(value)) value = '"'.join(value.split('"')) s = ''%(property, value, size) + elif isinstance(propclass, hyperdb.Password): + s = ''%(property, size) elif isinstance(propclass, hyperdb.Link): + sortfunc = self.make_sort_function(propclass.classname) linkcl = self.db.classes[propclass.classname] + options = linkcl.list() + options.sort(sortfunc) + # TODO: make this a field display, not a menu one! l = ['') s = '\n'.join(l) elif isinstance(propclass, hyperdb.Multilink): + sortfunc = self.make_sort_function(propclass.classname) linkcl = self.db.classes[propclass.classname] - list = linkcl.list() - height = height or min(len(list), 7) + if value: + value.sort(sortfunc) + # map the id to the label property + if not showid: + k = linkcl.labelprop() + value = [linkcl.get(v, k) for v in value] + value = cgi.escape(','.join(value)) + s = ''%(property, size, value) + else: + s = _('Plain: bad propclass "%(propclass)s"')%locals() + return s + + def do_multiline(self, property, rows=5, cols=40): + ''' display a string property in a multiline text edit field + ''' + if not self.nodeid and self.form is None and self.filterspec is None: + return _('[Multiline: not called from item]') + + propclass = self.properties[property] + + # make sure this is a link property + if not isinstance(propclass, hyperdb.String): + return _('[Multiline: not a string]') + + # get the value + value = self.determine_value(property) + if value is None: + value = '' + + # display + return ''%( + property, rows, cols, value) + + def do_menu(self, property, size=None, height=None, showid=0): + ''' for a Link property, display a menu of the available choices + ''' + if not self.nodeid and self.form is None and self.filterspec is None: + return _('[Field: not called from item]') + + propclass = self.properties[property] + + # make sure this is a link property + if not (isinstance(propclass, hyperdb.Link) or + isinstance(propclass, hyperdb.Multilink)): + return _('[Menu: not a link]') + + # sort function + sortfunc = self.make_sort_function(propclass.classname) + + # get the value + value = self.determine_value(property) + + # display + if isinstance(propclass, hyperdb.Multilink): + linkcl = self.db.classes[propclass.classname] + options = linkcl.list() + options.sort(sortfunc) + height = height or min(len(options), 7) l = ['') - s = '\n'.join(l) - else: - s = 'Plain: bad propclass "%s"'%propclass - return s - -class Menu(Base): - ''' for a Link property, display a menu of the available choices - ''' - def __call__(self, property, size=None, height=None, showid=0): - propclass = self.properties[property] - if self.nodeid: - value = self.cl.get(self.nodeid, property) - else: - # TODO: pull the value from the form - if isinstance(propclass, hyperdb.Multilink): value = [] - else: value = None + return '\n'.join(l) if isinstance(propclass, hyperdb.Link): + # force the value to be a single choice + if type(value) is types.ListType: + value = value[0] linkcl = self.db.classes[propclass.classname] l = ['') - return '\n'.join(l) - if isinstance(propclass, hyperdb.Multilink): - linkcl = self.db.classes[propclass.classname] - list = linkcl.list() - height = height or min(len(list), 7) - l = ['') return '\n'.join(l) - return '[Menu: not a link]' + return _('[Menu: not a link]') -#XXX deviates from spec -class Link(Base): - ''' for a Link or Multilink property, display the names of the linked - nodes, hyperlinked to the item views on those nodes - for other properties, link to this node with the property as the text - ''' - def __call__(self, property=None, **args): + #XXX deviates from spec + def do_link(self, property=None, is_download=0, showid=0): + '''For a Link or Multilink property, display the names of the linked + nodes, hyperlinked to the item views on those nodes. + For other properties, link to this node with the property as the + text. + + If is_download is true, append the property value to the generated + URL so that the link may be used as a download link and the + downloaded file name is correct. + ''' if not self.nodeid and self.form is None: - return '[Link: not called from item]' + return _('[Link: not called from item]') + + # get the value + value = self.determine_value(property) + if not value: + return _('[no %(propname)s]')%{'propname':property.capitalize()} + propclass = self.properties[property] - if self.nodeid: - value = self.cl.get(self.nodeid, property) - else: - if isinstance(propclass, hyperdb.Multilink): value = [] - else: value = '' if isinstance(propclass, hyperdb.Link): - if value is None: - return '[not assigned]' - linkcl = self.db.classes[propclass.classname] + linkname = propclass.classname + linkcl = self.db.classes[linkname] k = linkcl.labelprop() - linkvalue = linkcl.get(value, k) - return '%s'%(linkcl, value, linkvalue) + linkvalue = cgi.escape(str(linkcl.get(value, k))) + if showid: + label = value + title = ' title="%s"'%linkvalue + # note ... this should be urllib.quote(linkcl.get(value, k)) + else: + label = linkvalue + title = '' + if is_download: + return '%s'%(linkname, value, + linkvalue, title, label) + else: + return '%s'%(linkname, value, title, label) if isinstance(propclass, hyperdb.Multilink): - linkcl = self.db.classes[propclass.classname] + linkname = propclass.classname + linkcl = self.db.classes[linkname] k = linkcl.labelprop() l = [] for value in value: - linkvalue = linkcl.get(value, k) - l.append('%s'%(linkcl, value, linkvalue)) + linkvalue = cgi.escape(str(linkcl.get(value, k))) + if showid: + label = value + title = ' title="%s"'%linkvalue + # note ... this should be urllib.quote(linkcl.get(value, k)) + else: + label = linkvalue + title = '' + if is_download: + l.append('%s'%(linkname, value, + linkvalue, title, label)) + else: + l.append('%s'%(linkname, value, + title, label)) return ', '.join(l) - return '%s'%(self.classname, self.nodeid, value) + if is_download: + return '%s'%(self.classname, self.nodeid, + value, value) + else: + return '%s'%(self.classname, self.nodeid, value) -class Count(Base): - ''' for a Multilink property, display a count of the number of links in - the list - ''' - def __call__(self, property, **args): + def do_count(self, property, **args): + ''' for a Multilink property, display a count of the number of links in + the list + ''' if not self.nodeid: - return '[Count: not called from item]' + return _('[Count: not called from item]') + propclass = self.properties[property] + if not isinstance(propclass, hyperdb.Multilink): + return _('[Count: not a Multilink]') + + # figure the length then... value = self.cl.get(self.nodeid, property) - if isinstance(propclass, hyperdb.Multilink): - return str(len(value)) - return '[Count: not a Multilink]' + return str(len(value)) -# XXX pretty is definitely new ;) -class Reldate(Base): - ''' display a Date property in terms of an interval relative to the - current date (e.g. "+ 3w", "- 2d"). + # XXX pretty is definitely new ;) + def do_reldate(self, property, pretty=0): + ''' display a Date property in terms of an interval relative to the + current date (e.g. "+ 3w", "- 2d"). - with the 'pretty' flag, make it pretty - ''' - def __call__(self, property, pretty=0): + with the 'pretty' flag, make it pretty + ''' if not self.nodeid and self.form is None: - return '[Reldate: not called from item]' + return _('[Reldate: not called from item]') + propclass = self.properties[property] - if isinstance(not propclass, hyperdb.Date): - return '[Reldate: not a Date]' + if not isinstance(propclass, hyperdb.Date): + return _('[Reldate: not a Date]') + if self.nodeid: value = self.cl.get(self.nodeid, property) else: - value = date.Date('.') - interval = value - date.Date('.') + return '' + if not value: + return '' + + # figure the interval + interval = date.Date('.') - value if pretty: if not self.nodeid: - return 'now' - pretty = interval.pretty() - if pretty is None: - pretty = value.pretty() - return pretty + return _('now') + return interval.pretty() return str(interval) -class Download(Base): - ''' show a Link("file") or Multilink("file") property using links that - allow you to download files - ''' - def __call__(self, property, **args): + def do_download(self, property, **args): + ''' show a Link("file") or Multilink("file") property using links that + allow you to download files + ''' if not self.nodeid: - return '[Download: not called from item]' - propclass = self.properties[property] - value = self.cl.get(self.nodeid, property) - if isinstance(propclass, hyperdb.Link): - linkcl = self.db.classes[propclass.classname] - linkvalue = linkcl.get(value, k) - return '%s'%(linkcl, value, linkvalue) - if isinstance(propclass, hyperdb.Multilink): - linkcl = self.db.classes[propclass.classname] - l = [] - for value in value: - linkvalue = linkcl.get(value, k) - l.append('%s'%(linkcl, value, linkvalue)) - return ', '.join(l) - return '[Download: not a link]' + return _('[Download: not called from item]') + return self.do_link(property, is_download=1) -class Checklist(Base): - ''' for a Link or Multilink property, display checkboxes for the available - choices to permit filtering - ''' - def __call__(self, property, **args): + def do_checklist(self, property, **args): + ''' for a Link or Multilink property, display checkboxes for the + available choices to permit filtering + ''' propclass = self.properties[property] + if (not isinstance(propclass, hyperdb.Link) and not + isinstance(propclass, hyperdb.Multilink)): + return _('[Checklist: not a link]') + + # get our current checkbox state if self.nodeid: - value = self.cl.get(self.nodeid, property) + # get the info from the node - make sure it's a list + if isinstance(propclass, hyperdb.Link): + value = [self.cl.get(self.nodeid, property)] + else: + value = self.cl.get(self.nodeid, property) elif self.filterspec is not None: + # get the state from the filter specification (always a list) value = self.filterspec.get(property, []) else: + # it's a new node, so there's no state value = [] - if (isinstance(propclass, hyperdb.Link) or - isinstance(propclass, hyperdb.Multilink)): - linkcl = self.db.classes[propclass.classname] - l = [] - k = linkcl.labelprop() - for optionid in linkcl.list(): - option = linkcl.get(optionid, k) - if optionid in value or option in value: - checked = 'checked' - else: - checked = '' - l.append('%s:'%( - option, checked, property, option)) - return '\n'.join(l) - return '[Checklist: not a link]' -class Note(Base): - ''' display a "note" field, which is a text area for entering a note to - go along with a change. - ''' - def __call__(self, rows=5, cols=80): - # TODO: pull the value from the form - return ''%(rows, - cols) - -# XXX new function -class List(Base): - ''' list the items specified by property using the standard index for - the class - ''' - def __call__(self, property, **args): - propclass = self.properties[property] - if isinstance(not propclass, hyperdb.Multilink): - return '[List: not a Multilink]' + # so we can map to the linked node's "lable" property + linkcl = self.db.classes[propclass.classname] + l = [] + k = linkcl.labelprop() + for optionid in linkcl.list(): + option = cgi.escape(str(linkcl.get(optionid, k))) + if optionid in value or option in value: + checked = 'checked' + else: + checked = '' + l.append('%s:'%( + option, checked, property, option)) + + # for Links, allow the "unselected" option too + if isinstance(propclass, hyperdb.Link): + if value is None or '-1' in value: + checked = 'checked' + else: + checked = '' + l.append(_('[unselected]:')%(checked, property)) + return '\n'.join(l) + + def do_note(self, rows=5, cols=80): + ''' display a "note" field, which is a text area for entering a note to + go along with a change. + ''' + # TODO: pull the value from the form + return ''%(rows, cols) + + # XXX new function + def do_list(self, property, reverse=0): + ''' list the items specified by property using the standard index for + the class + ''' + propcl = self.properties[property] + if not isinstance(propcl, hyperdb.Multilink): + return _('[List: not a Multilink]') + + value = self.determine_value(property) + if not value: + return '' + + # sort, possibly revers and then re-stringify + value = map(int, value) + value.sort() + if reverse: + value.reverse() + value = map(str, value) + + # render the sub-index into a string fp = StringIO.StringIO() - args['show_display_form'] = 0 - value = self.cl.get(self.nodeid, property) - # TODO: really not happy with the way templates is passed on here - index(fp, self.templates, self.db, propclass.classname, nodeids=value, - show_display_form=0) + try: + write_save = self.client.write + self.client.write = fp.write + index = IndexTemplate(self.client, self.templates, propcl.classname) + index.render(nodeids=value, show_display_form=0) + finally: + self.client.write = write_save + return fp.getvalue() -# XXX new function -class History(Base): - ''' list the history of the item - ''' - def __call__(self, **args): + # XXX new function + def do_history(self, direction='descending'): + ''' list the history of the item + + If "direction" is 'descending' then the most recent event will + be displayed first. If it is 'ascending' then the oldest event + will be displayed first. + ''' if self.nodeid is None: - return "[History: node doesn't exist]" + return _("[History: node doesn't exist]") l = ['
Date | ', - 'User | ', - 'Action | ', - 'Args | '] - - for id, date, user, action, args in self.cl.history(self.nodeid): - l.append('
%s | %s | %s | %s | Date | '), + _('User | '), + _('Action | '), + _('Args | '), + ''] + + comments = {} + history = self.cl.history(self.nodeid) + history.sort() + if direction == 'descending': + history.reverse() + for id, evt_date, user, action, args in history: + date_s = str(evt_date).replace("."," ") + arg_s = '' + if action == 'link' and type(args) == type(()): + if len(args) == 3: + linkcl, linkid, key = args + arg_s += '%s%s %s'%(linkcl, linkid, + linkcl, linkid, key) + else: + arg_s = str(args) + + elif action == 'unlink' and type(args) == type(()): + if len(args) == 3: + linkcl, linkid, key = args + arg_s += '%s%s %s'%(linkcl, linkid, + linkcl, linkid, key) + else: + arg_s = str(args) + + elif type(args) == type({}): + cell = [] + for k in args.keys(): + # try to get the relevant property and treat it + # specially + try: + prop = self.properties[k] + except: + prop = None + if prop is not None: + if args[k] and (isinstance(prop, hyperdb.Multilink) or + isinstance(prop, hyperdb.Link)): + # figure what the link class is + classname = prop.classname + try: + linkcl = self.db.classes[classname] + except KeyError: + labelprop = None + comments[classname] = _('''The linked class + %(classname)s no longer exists''')%locals() + labelprop = linkcl.labelprop() + + if isinstance(prop, hyperdb.Multilink) and \ + len(args[k]) > 0: + ml = [] + for linkid in args[k]: + label = classname + linkid + # if we have a label property, try to use it + # TODO: test for node existence even when + # there's no labelprop! + try: + if labelprop is not None: + label = linkcl.get(linkid, labelprop) + except IndexError: + comments['no_link'] = _('''
---|---|---|---|
%s | %s | ' + '%s | %s |
Note: | |||
%s |