Mercurial > p > roundup > code
diff roundup/htmltemplate.py @ 25:4cf1daf2f2eb
More Grande Splite
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Sun, 22 Jul 2001 12:01:27 +0000 |
| parents | |
| children | c7c14960f413 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/roundup/htmltemplate.py Sun Jul 22 12:01:27 2001 +0000 @@ -0,0 +1,719 @@ +# $Id: htmltemplate.py,v 1.1 2001-07-22 11:58:35 richard Exp $ + +import os, re, StringIO, urllib, cgi + +import hyperdb, date + +class Base: + def __init__(self, db, templates, classname, nodeid=None, form=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 = form + self.cl = self.db.classes[self.classname] + self.properties = self.cl.getprops() + +class Plain(Base): + ''' 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) + ''' + def __call__(self, property): + if not self.nodeid and self.form is None: + return '[Field: not called from item]' + propclass = self.properties[property] + if self.nodeid: + value = self.cl.get(self.nodeid, property) + else: + # TODO: pull the value from the form + if propclass.isMultilinkType: value = [] + else: value = '' + if propclass.isStringType: + if value is None: value = '' + else: value = str(value) + elif propclass.isDateType: + value = str(value) + elif propclass.isIntervalType: + value = str(value) + elif propclass.isLinkType: + linkcl = self.db.classes[propclass.classname] + if value: value = str(linkcl.get(value, linkcl.getkey())) + else: value = '[unselected]' + elif propclass.isMultilinkType: + linkcl = self.db.classes[propclass.classname] + k = linkcl.getkey() + value = ', '.join([linkcl.get(i, k) for i in value]) + else: + s = 'Plain: bad propclass "%s"'%propclass + 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: + return '[Field: not called from item]' + propclass = self.properties[property] + if self.nodeid: + value = self.cl.get(self.nodeid, property) + else: + # TODO: pull the value from the form + if propclass.isMultilinkType: value = [] + else: value = '' + if (propclass.isStringType or propclass.isDateType or + propclass.isIntervalType): + size = size or 30 + if value is None: + value = '' + else: + value = cgi.escape(value) + value = '"'.join(value.split('"')) + s = '<input name="%s" value="%s" size="%s">'%(property, value, size) + elif propclass.isLinkType: + linkcl = self.db.classes[propclass.classname] + l = ['<select name="%s">'%property] + k = linkcl.getkey() + for optionid in linkcl.list(): + option = linkcl.get(optionid, k) + s = '' + if optionid == value: + s = 'selected ' + if showid: + lab = '%s%s: %s'%(propclass.classname, optionid, option) + else: + lab = option + if size is not None and len(lab) > size: + lab = lab[:size-3] + '...' + l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab)) + l.append('</select>') + s = '\n'.join(l) + elif propclass.isMultilinkType: + linkcl = self.db.classes[propclass.classname] + list = linkcl.list() + height = height or min(len(list), 7) + l = ['<select multiple name="%s" size="%s">'%(property, height)] + k = linkcl.getkey() + for optionid in list: + option = linkcl.get(optionid, k) + s = '' + if optionid in value: + s = 'selected ' + if showid: + lab = '%s%s: %s'%(propclass.classname, optionid, option) + else: + lab = option + if size is not None and len(lab) > size: + lab = lab[:size-3] + '...' + l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab)) + l.append('</select>') + 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 propclass.isMultilinkType: value = [] + else: value = None + if propclass.isLinkType: + linkcl = self.db.classes[propclass.classname] + l = ['<select name="%s">'%property] + k = linkcl.getkey() + for optionid in linkcl.list(): + option = linkcl.get(optionid, k) + s = '' + if optionid == value: + s = 'selected ' + l.append('<option %svalue="%s">%s</option>'%(s, optionid, option)) + l.append('</select>') + return '\n'.join(l) + if propclass.isMultilinkType: + linkcl = self.db.classes[propclass.classname] + list = linkcl.list() + height = height or min(len(list), 7) + l = ['<select multiple name="%s" size="%s">'%(property, height)] + k = linkcl.getkey() + for optionid in list: + option = linkcl.get(optionid, k) + s = '' + if optionid in value: + s = 'selected ' + if showid: + lab = '%s%s: %s'%(propclass.classname, optionid, option) + else: + lab = option + if size is not None and len(lab) > size: + lab = lab[:size-3] + '...' + l.append('<option %svalue="%s">%s</option>'%(s, optionid, option)) + l.append('</select>') + return '\n'.join(l) + 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): + if not self.nodeid and self.form is None: + return '[Link: not called from item]' + propclass = self.properties[property] + if self.nodeid: + value = self.cl.get(self.nodeid, property) + else: + if propclass.isMultilinkType: value = [] + else: value = '' + if propclass.isLinkType: + linkcl = self.db.classes[propclass.classname] + linkvalue = linkcl.get(value, k) + return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue) + if propclass.isMultilinkType: + linkcl = self.db.classes[propclass.classname] + l = [] + for value in value: + linkvalue = linkcl.get(value, k) + l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)) + return ', '.join(l) + return '<a href="%s%s">%s</a>'%(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): + if not self.nodeid: + return '[Count: not called from item]' + propclass = self.properties[property] + value = self.cl.get(self.nodeid, property) + if propclass.isMultilinkType: + return str(len(value)) + return '[Count: not a Multilink]' + +# 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"). + + with the 'pretty' flag, make it pretty + ''' + def __call__(self, property, pretty=0): + if not self.nodeid and self.form is None: + return '[Reldate: not called from item]' + propclass = self.properties[property] + if not propclass.isDateType: + return '[Reldate: not a Date]' + if self.nodeid: + value = self.cl.get(self.nodeid, property) + else: + value = date.Date('.') + interval = value - date.Date('.') + if pretty: + if not self.nodeid: + return 'now' + pretty = interval.pretty() + if pretty is None: + pretty = value.pretty() + return 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): + if not self.nodeid: + return '[Download: not called from item]' + propclass = self.properties[property] + value = self.cl.get(self.nodeid, property) + if propclass.isLinkType: + linkcl = self.db.classes[propclass.classname] + linkvalue = linkcl.get(value, k) + return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue) + if propclass.isMultilinkType: + linkcl = self.db.classes[propclass.classname] + l = [] + for value in value: + linkvalue = linkcl.get(value, k) + l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)) + return ', '.join(l) + return '[Download: not a link]' + + +class Checklist(Base): + ''' for a Link or Multilink property, display checkboxes for the available + choices to permit filtering + ''' + def __call__(self, property, **args): + propclass = self.properties[property] + if self.nodeid: + value = self.cl.get(self.nodeid, property) + else: + value = [] + if propclass.isLinkType or propclass.isMultilinkType: + linkcl = self.db.classes[propclass.classname] + l = [] + k = linkcl.getkey() + for optionid in linkcl.list(): + option = linkcl.get(optionid, k) + if optionid in value: + checked = 'checked' + else: + checked = '' + l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%( + option, checked, propclass.classname, 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 '<textarea name="__note" rows=%s cols=%s></textarea>'%(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 not propclass.isMultilinkType: + return '[List: not a Multilink]' + 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) + return fp.getvalue() + +# XXX new function +class History(Base): + ''' list the history of the item + ''' + def __call__(self, **args): + l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>', + '<tr class="list-header">', + '<td><span class="list-item"><strong>Date</strong></span></td>', + '<td><span class="list-item"><strong>User</strong></span></td>', + '<td><span class="list-item"><strong>Action</strong></span></td>', + '<td><span class="list-item"><strong>Args</strong></span></td>'] + + for id, date, user, action, args in self.cl.history(self.nodeid): + l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%( + date, user, action, args)) + l.append('</table>') + return '\n'.join(l) + +# XXX new function +class Submit(Base): + ''' add a submit button for the item + ''' + def __call__(self): + if self.nodeid: + return '<input type="submit" value="Submit Changes">' + elif self.form is not None: + return '<input type="submit" value="Submit New Entry">' + else: + return '[Submit: not called from item]' + + +# +# INDEX TEMPLATES +# +class IndexTemplateReplace: + def __init__(self, globals, locals, props): + self.globals = globals + self.locals = locals + self.props = props + + def go(self, text, replace=re.compile( + r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|' + r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)): + return 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')) + 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 == '-': + dir = '' + else: + name = name[1:] + if sort_name == name: + if dir == '': + s_dir = '-' + elif dir == '-': + 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) + +def index(client, templates, db, classname, filterspec={}, filter=[], + columns=[], sort=[], group=[], show_display_form=1, nodeids=None, + col_re=re.compile(r'<property\s+name="([^>]+)">')): + globals = { + 'plain': Plain(db, templates, classname, form={}), + 'field': Field(db, templates, classname, form={}), + 'menu': Menu(db, templates, classname, form={}), + 'link': Link(db, templates, classname, form={}), + 'count': Count(db, templates, classname, form={}), + 'reldate': Reldate(db, templates, classname, form={}), + 'download': Download(db, templates, classname, form={}), + 'checklist': Checklist(db, templates, classname, form={}), + 'list': List(db, templates, classname, form={}), + 'history': History(db, templates, classname, form={}), + 'submit': Submit(db, templates, classname, form={}), + 'note': Note(db, templates, classname, form={}) + } + cl = db.classes[classname] + properties = cl.getprops() + w = client.write + + try: + template = open(os.path.join(templates, classname+'.filter')).read() + all_filters = col_re.findall(template) + except IOError, error: + if error.errno != 2: raise + template = None + all_filters = [] + if template and filter: + # display the filter section + w('<form>') + w('<table width=100% border=0 cellspacing=0 cellpadding=2>') + w('<tr class="location-bar">') + w(' <th align="left" colspan="2">Filter specification...</th>') + w('</tr>') + replace = IndexTemplateReplace(globals, locals(), filter) + w(replace.go(template)) + if columns: + w('<input type="hidden" name=":columns" value="%s">'%','.join(columns)) + if filter: + w('<input type="hidden" name=":filter" value="%s">'%','.join(filter)) + if sort: + w('<input type="hidden" name=":sort" value="%s">'%','.join(sort)) + if group: + w('<input type="hidden" name=":group" value="%s">'%','.join(group)) + for k, v in filterspec.items(): + if type(v) == type([]): v = ','.join(v) + w('<input type="hidden" name="%s" value="%s">'%(k, v)) + w('<tr class="location-bar"><td width="1%%"> </td>') + w('<td><input type="submit" value="Redisplay"></td></tr>') + w('</table>') + w('</form>') + + # 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('<table width=100% border=0 cellspacing=0 cellpadding=2>') + w('<tr class="list-header">') + for name in columns: + cname = name.capitalize() + if show_display_form: + anchor = "%s?%s"%(classname, sortby(name, columns, filter, + sort, group, filterspec)) + w('<td><span class="list-item"><a href="%s">%s</a></span></td>'%( + anchor, cname)) + else: + w('<td><span class="list-item">%s</span></td>'%cname) + w('</tr>') + + # 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 prop.isLinkType: + 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 prop.isMultilinkType: + 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('<tr class="list-header">' + '<td align=left colspan=%s><strong>%s</strong></td></tr>'%( + 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('</table>') + + if not show_display_form: + return + + # now add in the filter/columns/group/etc config table form + w('<p><form>') + w('<table width=100% border=0 cellspacing=0 cellpadding=2>') + for k,v in filterspec.items(): + if type(v) == type([]): v = ','.join(v) + w('<input type="hidden" name="%s" value="%s">'%(k, v)) + if sort: + w('<input type="hidden" name=":sort" value="%s">'%','.join(sort)) + names = [] + for name in cl.getprops().keys(): + if name in all_filters or name in all_columns: + names.append(name) + w('<tr class="location-bar">') + w('<th align="left" colspan=%s>View customisation...</th></tr>'% + (len(names)+1)) + w('<tr class="location-bar"><th> </th>') + for name in names: + w('<th>%s</th>'%name.capitalize()) + w('</tr>') + + # filter + if all_filters: + w('<tr><th width="1%" align=right class="location-bar">Filters</th>') + for name in names: + if name not in all_filters: + w('<td> </td>') + continue + if name in filter: checked=' checked' + else: checked='' + w('<td align=middle>') + w('<input type="checkbox" name=":filter" value="%s" %s></td>'%(name, + checked)) + w('</tr>') + + # columns + if all_columns: + w('<tr><th width="1%" align=right class="location-bar">Columns</th>') + for name in names: + if name not in all_columns: + w('<td> </td>') + continue + if name in columns: checked=' checked' + else: checked='' + w('<td align=middle>') + w('<input type="checkbox" name=":columns" value="%s" %s></td>'%( + name, checked)) + w('</tr>') + + # group + w('<tr><th width="1%" align=right class="location-bar">Grouping</th>') + for name in names: + prop = properties[name] + if name not in all_columns: + w('<td> </td>') + continue + if name in group: checked=' checked' + else: checked='' + w('<td align=middle>') + w('<input type="checkbox" name=":group" value="%s" %s></td>'%( + name, checked)) + w('</tr>') + + w('<tr class="location-bar"><td width="1%"> </td>') + w('<td colspan="%s">'%len(names)) + w('<input type="submit" value="Redisplay"></td></tr>') + w('</table>') + w('</form>') + + +# +# ITEM TEMPLATES +# +class ItemTemplateReplace: + 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'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|' + r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)): + return replace.sub(self, text) + + def __call__(self, m, filter=None, columns=None, sort=None, group=None): + if m.group('name'): + if self.nodeid and self.cl.get(self.nodeid, m.group('name')): + replace = ItemTemplateReplace(self.globals, {}, self.cl, + self.nodeid) + return replace.go(m.group('text')) + else: + return '' + 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<prop><property\s+name="(?P<propname>[^>]+)">)|' + r'(?P<endprop></property>)|' + r'(?P<display><display\s+call="(?P<command>[^"]+)">))', 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('<form action="%s%s">'%(classname, nodeid)) + s = open(os.path.join(templates, classname+'.item')).read() + replace = ItemTemplateReplace(globals, locals(), cl, nodeid) + w(replace.go(s)) + w('</form>') + + +def newitem(client, templates, db, classname, form, replace=re.compile( + r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|' + r'(?P<endprop></property>)|' + r'(?P<display><display\s+call="(?P<command>[^"]+)">))', 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('<form action="new%s">'%classname) + replace = ItemTemplateReplace(globals, locals(), None, None) + w(replace.go(s)) + w('</form>') + +# +# $Log: not supported by cvs2svn $ +# Revision 1.5 2001/07/20 07:34:43 richard +# Quote the value put in the text input value attribute. +# +# Revision 1.4 2001/07/19 06:27:07 anthonybaxter +# fixing (manually) the (dollarsign)Log(dollarsign) entries caused by +# my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign) +# strings in a commit message. I'm a twonk. +# +# Also broke the help string in two. +# +# Revision 1.3 2001/07/19 05:52:22 anthonybaxter +# Added CVS keywords Id and Log to all python files. +# +# +
