X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fhtmltemplate.py;h=2f6557a1fdb0abba2b3e4d660048c9cfdc2e923b;hb=f04ca7e8c6067e05296508ed2b7c302425ffbcc8;hp=c073a75e9ea357d6fe95227a98a775cc47dc9dcc;hpb=729c4981a73b1eadd183066b120f65d6b9d0db19;p=roundup.git diff --git a/roundup/htmltemplate.py b/roundup/htmltemplate.py index c073a75..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.24 2001-09-27 06:45:58 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, escape=0): + 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,64 +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: - return cgi.escape(value) + 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): linkname = propclass.classname - if value is None: - return '[not assigned]' linkcl = self.db.classes[linkname] k = linkcl.labelprop() - linkvalue = linkcl.get(value, k) - return '%s'%(linkname, 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): linkname = propclass.classname linkcl = self.db.classes[linkname] k = linkcl.labelprop() l = [] for value in value: - linkvalue = linkcl.get(value, k) - l.append('%s'%(linkname, 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, reverse=0): - propclass = self.properties[property] - if isinstance(not propclass, hyperdb.Multilink): - return '[List: not a Multilink]' - fp = StringIO.StringIO() - value = self.cl.get(self.nodeid, property) + # 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() - # 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) + value = map(str, value) + + # render the sub-index into a string + fp = StringIO.StringIO() + 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 = ['', '', - '', - '', - '', - ''] - - for id, date, user, action, args in self.cl.history(self.nodeid): - l.append(''%( - 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'] = _('''The + linked node no longer + exists''') + ml.append('%s'%label) + else: + ml.append('%s'%( + classname, linkid, label)) + cell.append('%s:\n %s'%(k, ',\n '.join(ml))) + elif isinstance(prop, hyperdb.Link) and args[k]: + label = classname + args[k] + # if we have a label property, try to use it + # TODO: test for node existence even when + # there's no labelprop! + if labelprop is not None: + try: + label = linkcl.get(args[k], labelprop) + except IndexError: + comments['no_link'] = _('''The + linked node no longer + exists''') + cell.append(' %s,\n'%label) + # "flag" this is done .... euwww + label = None + if label is not None: + cell.append('%s: %s\n'%(k, + classname, args[k], label)) + + elif isinstance(prop, hyperdb.Date) and args[k]: + d = date.Date(args[k]) + cell.append('%s: %s'%(k, str(d))) + + elif isinstance(prop, hyperdb.Interval) and args[k]: + d = date.Interval(args[k]) + cell.append('%s: %s'%(k, str(d))) + + elif not args[k]: + cell.append('%s: (no value)\n'%k) + + else: + cell.append('%s: %s\n'%(k, str(args[k]))) + else: + # property no longer exists + comments['no_exist'] = _('''The indicated property + no longer exists''') + cell.append('%s: %s\n'%(k, str(args[k]))) + arg_s = '
'.join(cell) + else: + # unkown event!! + comments['unknown'] = _('''This event is not + handled by the history display!''') + arg_s = '' + str(args) + '' + date_s = date_s.replace(' ', ' ') + l.append('' + ''%(date_s, + user, action, arg_s)) + if comments: + l.append(_('')) + for entry in comments.values(): + l.append(''%entry) l.append('
DateUserActionArgs
%s%s%s%s
DateUserActionArgs
%s%s%s%s
Note:
%s
') return '\n'.join(l) -# XXX new function -class Submit(Base): - ''' add a submit button for the item - ''' - def __call__(self): + # XXX new function + def do_submit(self): + ''' add a submit button for the item + ''' if self.nodeid: - return '' + return _('') elif self.form is not None: - return '' + return _('') else: - return '[Submit: not called from item]' + return _('[Submit: not called from item]') + + def do_classhelp(self, classname, properties, label='?', width='400', + 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'). + You may optionally override the label displayed, the width and + height. The popup window will be resizable and scrollable. + ''' + return '(%s)'%(classname, + properties, width, height, label) # # INDEX TEMPLATES # class IndexTemplateReplace: + '''Regular-expression based parser that turns the template into HTML. + ''' def __init__(self, globals, locals, props): self.globals = globals self.locals = locals self.props = props - def go(self, text, replace=re.compile( - r'(([^>]+)">(?P.+?))|' - r'(?P[^"]+)">))', re.I|re.S)): - return replace.sub(self, text) + replace=re.compile( + r'(([^>]+)">(?P.+?))|' + r'(?P[^"]+)">))', re.I|re.S) + def go(self, text): + return self.replace.sub(self, text) def __call__(self, m, filter=None, columns=None, sort=None, group=None): if m.group('name'): if m.group('name') in self.props: text = m.group('text') replace = IndexTemplateReplace(self.globals, {}, self.props) - return replace.go(m.group('text')) + return replace.go(text) else: return '' if m.group('display'): command = m.group('command') return eval(command, self.globals, self.locals) - print '*** unhandled match', m.groupdict() - -def sortby(sort_name, columns, filter, sort, group, filterspec): - l = [] - w = l.append - for k, v in filterspec.items(): - k = urllib.quote(k) - if type(v) == type([]): - w('%s=%s'%(k, ','.join(map(urllib.quote, v)))) - else: - w('%s=%s'%(k, urllib.quote(v))) - if columns: - w(':columns=%s'%','.join(map(urllib.quote, columns))) - if filter: - w(':filter=%s'%','.join(map(urllib.quote, filter))) - if group: - w(':group=%s'%','.join(map(urllib.quote, group))) - m = [] - s_dir = '' - for name in sort: - dir = name[0] - if dir == '-': - name = name[1:] - else: - dir = '' - if sort_name == name: - if dir == '-': - s_dir = '' - else: - s_dir = '-' + return '*** unhandled match: %s'%str(m.groupdict()) + +class IndexTemplate(TemplateFunctions): + '''Templating functionality specifically for index pages + ''' + def __init__(self, client, templates, classname): + TemplateFunctions.__init__(self) + self.client = client + self.instance = client.instance + self.templates = templates + self.classname = classname + + # derived + self.db = self.client.db + self.cl = self.db.classes[self.classname] + self.properties = self.cl.getprops() + + col_re=re.compile(r']+)">') + def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[], + show_display_form=1, nodeids=None, show_customization=1): + self.filterspec = filterspec + + w = self.client.write + + # get the filter template + try: + filter_template = open(os.path.join(self.templates, + self.classname+'.filter')).read() + all_filters = self.col_re.findall(filter_template) + except IOError, error: + if error.errno not in (errno.ENOENT, errno.ESRCH): raise + filter_template = None + all_filters = [] + + # XXX deviate from spec here ... + # load the index section template and figure the default columns from it + try: + template = open(os.path.join(self.templates, + self.classname+'.index')).read() + except IOError, error: + if error.errno not in (errno.ENOENT, errno.ESRCH): raise + raise MissingTemplateError, self.classname+'.index' + all_columns = self.col_re.findall(template) + if not columns: + columns = [] + for name in all_columns: + columns.append(name) else: - m.append(dir+urllib.quote(name)) - m.insert(0, s_dir+urllib.quote(sort_name)) - # so things don't get completely out of hand, limit the sort to two columns - w(':sort=%s'%','.join(m[:2])) - return '&'.join(l) - -def index(client, templates, db, classname, filterspec={}, filter=[], - columns=[], sort=[], group=[], show_display_form=1, nodeids=None, - col_re=re.compile(r']+)">')): - globals = { - 'plain': Plain(db, templates, classname, filterspec=filterspec), - 'field': Field(db, templates, classname, filterspec=filterspec), - 'menu': Menu(db, templates, classname, filterspec=filterspec), - 'link': Link(db, templates, classname, filterspec=filterspec), - 'count': Count(db, templates, classname, filterspec=filterspec), - 'reldate': Reldate(db, templates, classname, filterspec=filterspec), - 'download': Download(db, templates, classname, filterspec=filterspec), - 'checklist': Checklist(db, templates, classname, filterspec=filterspec), - 'list': List(db, templates, classname, filterspec=filterspec), - 'history': History(db, templates, classname, filterspec=filterspec), - 'submit': Submit(db, templates, classname, filterspec=filterspec), - 'note': Note(db, templates, classname, filterspec=filterspec) - } - cl = db.classes[classname] - properties = cl.getprops() - w = client.write - w('
') - - try: - template = open(os.path.join(templates, classname+'.filter')).read() - all_filters = col_re.findall(template) - except IOError, error: - if error.errno != errno.ENOENT: raise - template = None - all_filters = [] - if template and filter: + # re-sort columns to be the same order as all_columns + l = [] + for name in all_columns: + if name in columns: + l.append(name) + columns = l + # display the filter section - w('') - w('') - w(' ') - w('') - replace = IndexTemplateReplace(globals, locals(), filter) - w(replace.go(template)) - w('') - w('') + if (show_display_form and + self.instance.FILTER_POSITION in ('top and bottom', 'top')): + w('\n'%self.classname) + self.filter_section(filter_template, filter, columns, group, + all_filters, all_columns, show_customization) + # make sure that the sorting doesn't get lost either + if sort: + w(''% + ','.join(sort)) + w('\n') + + + # now display the index section + w('
Filter specification...
 
\n') + w('\n') + for name in columns: + cname = name.capitalize() + if show_display_form: + sb = self.sortby(name, filterspec, columns, filter, group, sort) + anchor = "%s?%s"%(self.classname, sb) + w('\n'%( + anchor, cname)) + else: + w('\n'%cname) + w('\n') + + # this stuff is used for group headings - optimise the group names + old_group = None + group_names = [] + if group: + for name in group: + if name[0] == '-': group_names.append(name[1:]) + else: group_names.append(name) + + # now actually loop through all the nodes we get from the filter and + # apply the template + if nodeids is None: + nodeids = self.cl.filter(filterspec, sort, group) + for nodeid in nodeids: + # check for a group heading + if group_names: + this_group = [self.cl.get(nodeid, name, _('[no value]')) + for name in group_names] + if this_group != old_group: + l = [] + for name in group_names: + prop = self.properties[name] + if isinstance(prop, hyperdb.Link): + group_cl = self.db.classes[prop.classname] + key = group_cl.getkey() + value = self.cl.get(nodeid, name) + if value is None: + l.append(_('[unselected %(classname)s]')%{ + 'classname': prop.classname}) + else: + l.append(group_cl.get(self.cl.get(nodeid, + name), key)) + elif isinstance(prop, hyperdb.Multilink): + group_cl = self.db.classes[prop.classname] + key = group_cl.getkey() + for value in self.cl.get(nodeid, name): + l.append(group_cl.get(value, key)) + else: + value = self.cl.get(nodeid, name, _('[no value]')) + if value is None: + value = _('[empty %(name)s]')%locals() + else: + value = str(value) + l.append(value) + w('' + ''%( + len(columns), ', '.join(l))) + old_group = this_group + + # display this node's row + replace = IndexTemplateReplace(self.globals, locals(), columns) + self.nodeid = nodeid + w(replace.go(template)) + self.nodeid = None + w('
%s%s
%s
') - # If the filters aren't being displayed, then hide their current - # value in the form - if not filter: - for k, v in filterspec.items(): - if type(v) == type([]): v = ','.join(v) - w(''%(k, v)) - - # make sure that the sorting doesn't get lost either - if sort: - w(''%','.join(sort)) - - # XXX deviate from spec here ... - # load the index section template and figure the default columns from it - template = open(os.path.join(templates, classname+'.index')).read() - all_columns = col_re.findall(template) - if not columns: - columns = [] - for name in all_columns: - columns.append(name) - else: - # re-sort columns to be the same order as all_columns - l = [] - for name in all_columns: - if name in columns: - l.append(name) - columns = l - - # now display the index section - w('\n') - w('\n') - for name in columns: - cname = name.capitalize() - if show_display_form: - anchor = "%s?%s"%(classname, sortby(name, columns, filter, - sort, group, filterspec)) - w('\n'%( - anchor, cname)) + # display the filter section + if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and + self.instance.FILTER_POSITION in ('top and bottom', 'bottom')): + w('\n'%self.classname) + self.filter_section(filter_template, filter, columns, group, + all_filters, all_columns, show_customization) + # make sure that the sorting doesn't get lost either + if sort: + w(''% + ','.join(sort)) + w('\n') + + + def filter_section(self, template, filter, columns, group, all_filters, + all_columns, show_customization): + + w = self.client.write + + # wrap the template in a single table to ensure the whole widget + # is displayed at once + w('
%s
') for name in names: - if name not in all_columns: - w('') - continue - if name in columns: checked=' checked' - else: checked='' - w('\n'%( - name, checked)) + w(''%name.capitalize()) w('\n') - # group - w('\n') - for name in names: - prop = properties[name] - if name not in all_columns: - w('') - continue - if name in group: checked=' checked' - else: checked='' - w('\n'%( - name, checked)) + # Filter + if all_filters: + w(_('\n')) + for name in names: + if name not in all_filters: + w('') + continue + if name in filter: checked=' checked' + else: checked='' + w('\n'%(name, checked)) + w('\n') + + # Columns + if all_columns: + w(_('\n')) + for name in names: + if name not in all_columns: + w('') + continue + if name in columns: checked=' checked' + else: checked='' + w('\n'%(name, checked)) + w('\n') + + # Grouping + w(_('\n')) + for name in names: + if name not in all_columns: + w('') + continue + if name in group: checked=' checked' + else: checked='' + w('\n'%(name, checked)) + w('\n') + + w('') + w('')) w('\n') + w('
') + + if template and filter: + # display the filter section + w('') + w('') + w(_(' ')) + w('') + replace = IndexTemplateReplace(self.globals, locals(), filter) + w(replace.go(template)) + w('') + w(_('')) + w('
Filter specification...
 
') + + # now add in the filter/columns/group/etc config table form + w('' % + show_customization ) + w('\n') + names = [] + seen = {} + for name in all_filters + all_columns: + if self.properties.has_key(name) and not seen.has_key(name): + names.append(name) + seen[name] = 1 + if show_customization: + action = '-' else: - w('\n'%cname) - w('\n') - - # this stuff is used for group headings - optimise the group names - old_group = None - group_names = [] - if group: - for name in group: - if name[0] == '-': group_names.append(name[1:]) - else: group_names.append(name) - - # now actually loop through all the nodes we get from the filter and - # apply the template - if nodeids is None: - nodeids = cl.filter(filterspec, sort, group) - for nodeid in nodeids: - # check for a group heading - if group_names: - this_group = [cl.get(nodeid, name) for name in group_names] - if this_group != old_group: - l = [] - for name in group_names: - prop = properties[name] - if isinstance(prop, hyperdb.Link): - group_cl = db.classes[prop.classname] - key = group_cl.getkey() - value = cl.get(nodeid, name) - if value is None: - l.append('[unselected %s]'%prop.classname) - else: - l.append(group_cl.get(cl.get(nodeid, name), key)) - elif isinstance(prop, hyperdb.Multilink): - group_cl = db.classes[prop.classname] - key = group_cl.getkey() - for value in cl.get(nodeid, name): - l.append(group_cl.get(value, key)) - else: - value = cl.get(nodeid, name) - if value is None: - value = '[empty %s]'%name - l.append(value) - w('' - ''%( - len(columns), ', '.join(l))) - old_group = this_group - - # display this node's row - for value in globals.values(): - if hasattr(value, 'nodeid'): - value.nodeid = nodeid - replace = IndexTemplateReplace(globals, locals(), columns) - w(replace.go(template)) - - w('
%s
%s
') - - if not show_display_form: - return - - # now add in the filter/columns/group/etc config table form - w('

') - w('\n') - names = [] - for name in cl.getprops().keys(): - if name in all_filters or name in all_columns: - names.append(name) - w('') - w('\n'% - (len(names)+1)) - w('') - for name in names: - w(''%name.capitalize()) - w('\n') - - # filter - if all_filters: - w('\n') - for name in names: - if name not in all_filters: - w('') - continue - if name in filter: checked=' checked' - else: checked='' - w('\n'%( - name, checked)) - w('\n') + action = '+' + # hide the values for filters, columns and grouping in the form + # if the customization widget is not visible + for name in names: + if all_filters and name in filter: + w('' % name) + if all_columns and name in columns: + w('' % name) + if all_columns and name in group: + w('' % name) + + # TODO: The widget style can go into the stylesheet + w(_('\n')%(len(names)+1, action)) - # columns - if all_columns: - w('\n') + if not show_customization: + w('
View customisation...
 %s
Filters \n') - w('
' + ' View ' + 'customisation...
Columns
\n') + return + + w('

  \n') - w(' %s
Grouping \n') - w('
Filters \n') + w('
Columns \n') + w('
Grouping \n') + w('
 '%len(names)) + w(_('
\n') - w(' ') - w(''%len(names)) - w('\n') - w('\n') - w('\n') + # and the outer table + w('') + def sortby(self, sort_name, filterspec, columns, filter, group, sort): + l = [] + w = l.append + for k, v in filterspec.items(): + k = urllib.quote(k) + if type(v) == type([]): + w('%s=%s'%(k, ','.join(map(urllib.quote, v)))) + else: + w('%s=%s'%(k, urllib.quote(v))) + if columns: + w(':columns=%s'%','.join(map(urllib.quote, columns))) + if filter: + w(':filter=%s'%','.join(map(urllib.quote, filter))) + if group: + w(':group=%s'%','.join(map(urllib.quote, group))) + m = [] + s_dir = '' + for name in sort: + dir = name[0] + if dir == '-': + name = name[1:] + else: + dir = '' + if sort_name == name: + if dir == '-': + s_dir = '' + else: + s_dir = '-' + else: + m.append(dir+urllib.quote(name)) + m.insert(0, s_dir+urllib.quote(sort_name)) + # so things don't get completely out of hand, limit the sort to + # two columns + w(':sort=%s'%','.join(m[:2])) + return '&'.join(l) + # # ITEM TEMPLATES # class ItemTemplateReplace: + '''Regular-expression based parser that turns the template into HTML. + ''' def __init__(self, globals, locals, cl, nodeid): self.globals = globals self.locals = locals self.cl = cl self.nodeid = nodeid - def go(self, text, replace=re.compile( - r'(([^>]+)">(?P.+?))|' - r'(?P[^"]+)">))', re.I|re.S)): - return replace.sub(self, text) + replace=re.compile( + r'(([^>]+)">(?P.+?))|' + r'(?P[^"]+)">))', re.I|re.S) + def go(self, text): + return self.replace.sub(self, text) def __call__(self, m, filter=None, columns=None, sort=None, group=None): if m.group('name'): @@ -668,85 +1051,353 @@ class ItemTemplateReplace: if m.group('display'): command = m.group('command') return eval(command, self.globals, self.locals) - print '*** unhandled match', m.groupdict() - -def item(client, templates, db, classname, nodeid, replace=re.compile( - r'((?P[^>]+)">)|' - r'(?P)|' - r'(?P[^"]+)">))', re.I)): - - globals = { - 'plain': Plain(db, templates, classname, nodeid), - 'field': Field(db, templates, classname, nodeid), - 'menu': Menu(db, templates, classname, nodeid), - 'link': Link(db, templates, classname, nodeid), - 'count': Count(db, templates, classname, nodeid), - 'reldate': Reldate(db, templates, classname, nodeid), - 'download': Download(db, templates, classname, nodeid), - 'checklist': Checklist(db, templates, classname, nodeid), - 'list': List(db, templates, classname, nodeid), - 'history': History(db, templates, classname, nodeid), - 'submit': Submit(db, templates, classname, nodeid), - 'note': Note(db, templates, classname, nodeid) - } - - cl = db.classes[classname] - properties = cl.getprops() - - if properties.has_key('type') and properties.has_key('content'): - pass - # XXX we really want to return this as a downloadable... - # currently I handle this at a higher level by detecting 'file' - # designators... - - w = client.write - w('
'%(classname, nodeid)) - s = open(os.path.join(templates, classname+'.item')).read() - replace = ItemTemplateReplace(globals, locals(), cl, nodeid) - w(replace.go(s)) - w('
') - - -def newitem(client, templates, db, classname, form, replace=re.compile( - r'((?P[^>]+)">)|' - r'(?P)|' - r'(?P[^"]+)">))', re.I)): - globals = { - 'plain': Plain(db, templates, classname, form=form), - 'field': Field(db, templates, classname, form=form), - 'menu': Menu(db, templates, classname, form=form), - 'link': Link(db, templates, classname, form=form), - 'count': Count(db, templates, classname, form=form), - 'reldate': Reldate(db, templates, classname, form=form), - 'download': Download(db, templates, classname, form=form), - 'checklist': Checklist(db, templates, classname, form=form), - 'list': List(db, templates, classname, form=form), - 'history': History(db, templates, classname, form=form), - 'submit': Submit(db, templates, classname, form=form), - 'note': Note(db, templates, classname, form=form) - } - - cl = db.classes[classname] - properties = cl.getprops() - - w = client.write - try: - s = open(os.path.join(templates, classname+'.newitem')).read() - except: - s = open(os.path.join(templates, classname+'.item')).read() - w('
'%classname) - for key in form.keys(): - if key[0] == ':': - value = form[key].value - if type(value) != type([]): value = [value] - for value in value: - w(''%(key, value)) - replace = ItemTemplateReplace(globals, locals(), None, None) - w(replace.go(s)) - w('
') + return '*** unhandled match: %s'%str(m.groupdict()) + + +class ItemTemplate(TemplateFunctions): + '''Templating functionality specifically for item (node) display + ''' + def __init__(self, client, templates, classname): + TemplateFunctions.__init__(self) + self.client = client + self.instance = client.instance + self.templates = templates + self.classname = classname + + # derived + self.db = self.client.db + self.cl = self.db.classes[self.classname] + self.properties = self.cl.getprops() + + def render(self, nodeid): + self.nodeid = nodeid + + if (self.properties.has_key('type') and + self.properties.has_key('content')): + pass + # XXX we really want to return this as a downloadable... + # currently I handle this at a higher level by detecting 'file' + # designators... + + w = self.client.write + w('
'%( + self.classname, nodeid)) + s = open(os.path.join(self.templates, self.classname+'.item')).read() + replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid) + w(replace.go(s)) + w('
') + + +class NewItemTemplate(TemplateFunctions): + '''Templating functionality specifically for NEW item (node) display + ''' + def __init__(self, client, templates, classname): + TemplateFunctions.__init__(self) + self.client = client + self.instance = client.instance + self.templates = templates + self.classname = classname + + # derived + self.db = self.client.db + self.cl = self.db.classes[self.classname] + self.properties = self.cl.getprops() + + def render(self, form): + self.form = form + w = self.client.write + c = self.classname + try: + s = open(os.path.join(self.templates, c+'.newitem')).read() + except IOError: + s = open(os.path.join(self.templates, c+'.item')).read() + w('
'%c) + for key in form.keys(): + if key[0] == ':': + value = form[key].value + if type(value) != type([]): value = [value] + for value in value: + w(''%(key, value)) + replace = ItemTemplateReplace(self.globals, locals(), None, None) + w(replace.go(s)) + w('
') # # $Log: not supported by cvs2svn $ +# Revision 1.88 2002/04/24 08:34:35 rochecompaan +# Sorting was applied to all nodes of the MultiLink class instead of +# the nodes that are actually linked to in the "field" template +# function. This adds about 20+ seconds in the display of an issue if +# your database has a 1000 or more issue in it. +# +# Revision 1.87 2002/04/03 06:12:46 richard +# Fix for date properties as labels. +# +# Revision 1.86 2002/04/03 05:54:31 richard +# Fixed serialisation problem by moving the serialisation step out of the +# hyperdb.Class (get, set) into the hyperdb.Database. +# +# Also fixed htmltemplate after the showid changes I made yesterday. +# +# Unit tests for all of the above written. +# +# Revision 1.85 2002/04/02 01:40:58 richard +# . link() htmltemplate function now has a "showid" option for links and +# multilinks. When true, it only displays the linked node id as the anchor +# text. The link value is displayed as a tooltip using the title anchor +# attribute. +# +# Revision 1.84 2002/03/29 19:41:48 rochecompaan +# . Fixed display of mutlilink properties when using the template +# functions, menu and plain. +# +# Revision 1.83 2002/02/27 04:14:31 richard +# Ran it through pychecker, made fixes +# +# Revision 1.82 2002/02/21 23:11:45 richard +# . fixed some problems in date calculations (calendar.py doesn't handle over- +# and under-flow). Also, hour/minute/second intervals may now be more than +# 99 each. +# +# Revision 1.81 2002/02/21 07:21:38 richard +# docco +# +# Revision 1.80 2002/02/21 07:19:08 richard +# ... and label, width and height control for extra flavour! +# +# Revision 1.79 2002/02/21 06:57:38 richard +# . Added popup help for classes using the classhelp html template function. +# - add +# to an item page, and it generates a link to a popup window which displays +# the id, name and description for the priority class. The description +# field won't exist in most installations, but it will be added to the +# default templates. +# +# Revision 1.78 2002/02/21 06:23:00 richard +# *** empty log message *** +# +# Revision 1.77 2002/02/20 05:05:29 richard +# . Added simple editing for classes that don't define a templated interface. +# - access using the admin "class list" interface +# - limited to admin-only +# - requires the csv module from object-craft (url given if it's missing) +# +# Revision 1.76 2002/02/16 09:10:52 richard +# oops +# +# Revision 1.75 2002/02/16 08:43:23 richard +# . #517906 ] Attribute order in "View customisation" +# +# Revision 1.74 2002/02/16 08:39:42 richard +# . #516854 ] "My Issues" and redisplay +# +# Revision 1.73 2002/02/15 07:08:44 richard +# . Alternate email addresses are now available for users. See the MIGRATION +# file for info on how to activate the feature. +# +# Revision 1.72 2002/02/14 23:39:18 richard +# . All forms now have "double-submit" protection when Javascript is enabled +# on the client-side. +# +# Revision 1.71 2002/01/23 06:15:24 richard +# real (non-string, duh) sorting of lists by node id +# +# Revision 1.70 2002/01/23 05:47:57 richard +# more HTML template cleanup and unit tests +# +# Revision 1.69 2002/01/23 05:10:27 richard +# More HTML template cleanup and unit tests. +# - download() now implemented correctly, replacing link(is_download=1) [fixed in the +# templates, but link(is_download=1) will still work for existing templates] +# +# Revision 1.68 2002/01/22 22:55:28 richard +# . htmltemplate list() wasn't sorting... +# +# Revision 1.67 2002/01/22 22:46:22 richard +# more htmltemplate cleanups and unit tests +# +# Revision 1.66 2002/01/22 06:35:40 richard +# more htmltemplate tests and cleanup +# +# Revision 1.65 2002/01/22 00:12:06 richard +# Wrote more unit tests for htmltemplate, and while I was at it, I polished +# off the implementation of some of the functions so they behave sanely. +# +# Revision 1.64 2002/01/21 03:25:59 richard +# oops +# +# Revision 1.63 2002/01/21 02:59:10 richard +# Fixed up the HTML display of history so valid links are actually displayed. +# Oh for some unit tests! :( +# +# Revision 1.62 2002/01/18 08:36:12 grubert +# . add nowrap to history table date cell i.e.