X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fcgi%2Ftemplating.py;h=a9757fa35bba8716ded4b9cc4b0f9420b3151911;hb=97429ed8b266654ea8d987504967371e12c40b4d;hp=aea865120c74e74ecc7e398f55453e46c2ceac06;hpb=dfa6a36380240155b412ba1a2d2bb0b1689b41f6;p=roundup.git diff --git a/roundup/cgi/templating.py b/roundup/cgi/templating.py index aea8651..a9757fa 100644 --- a/roundup/cgi/templating.py +++ b/roundup/cgi/templating.py @@ -16,111 +16,94 @@ try: except ImportError: StructuredText = None -# Make sure these modules are loaded -# I need these to run PageTemplates outside of Zope :( -# If we're running in a Zope environment, these modules will be loaded -# already... -if not sys.modules.has_key('zLOG'): - import zLOG - sys.modules['zLOG'] = zLOG -if not sys.modules.has_key('MultiMapping'): - import MultiMapping - sys.modules['MultiMapping'] = MultiMapping -if not sys.modules.has_key('ComputedAttribute'): - import ComputedAttribute - sys.modules['ComputedAttribute'] = ComputedAttribute -if not sys.modules.has_key('ExtensionClass'): - import ExtensionClass - sys.modules['ExtensionClass'] = ExtensionClass -if not sys.modules.has_key('Acquisition'): - import Acquisition - sys.modules['Acquisition'] = Acquisition - -# now it's safe to import PageTemplates, TAL and ZTUtils -from PageTemplates import PageTemplate -from PageTemplates.Expressions import getEngine -from TAL.TALInterpreter import TALInterpreter -import ZTUtils - -# XXX WAH pagetemplates aren't pickleable :( -#def getTemplate(dir, name, classname=None, request=None): -# ''' Interface to get a template, possibly loading a compiled template. -# ''' -# # source -# src = os.path.join(dir, name) -# -# # see if we can get a compile from the template"c" directory (most -# # likely is "htmlc" -# split = list(os.path.split(dir)) -# split[-1] = split[-1] + 'c' -# cdir = os.path.join(*split) -# split.append(name) -# cpl = os.path.join(*split) -# -# # ok, now see if the source is newer than the compiled (or if the -# # compiled even exists) -# MTIME = os.path.stat.ST_MTIME -# if (not os.path.exists(cpl) or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]): -# # nope, we need to compile -# pt = RoundupPageTemplate() -# pt.write(open(src).read()) -# pt.id = name -# -# # save off the compiled template -# if not os.path.exists(cdir): -# os.makedirs(cdir) -# f = open(cpl, 'wb') -# pickle.dump(pt, f) -# f.close() -# else: -# # yay, use the compiled template -# f = open(cpl, 'rb') -# pt = pickle.load(f) -# return pt - -templates = {} - -def getTemplate(dir, name, extension, classname=None, request=None): - ''' Interface to get a template, possibly loading a compiled template. - - "name" and "extension" indicate the template we're after, which in - most cases will be "name.extension". If "extension" is None, then - we look for a template just called "name" with no extension. - - If the file "name.extension" doesn't exist, we look for - "_generic.extension" as a fallback. - ''' - # default the name to "home" - if name is None: - name = 'home' +# bring in the templating support +from roundup.cgi.PageTemplates import PageTemplate +from roundup.cgi.PageTemplates.Expressions import getEngine +from roundup.cgi.TAL.TALInterpreter import TALInterpreter +from roundup.cgi import ZTUtils - # find the source, figure the time it was last modified - if extension: - filename = '%s.%s'%(name, extension) - else: - filename = name - src = os.path.join(dir, filename) - try: - stime = os.stat(src)[os.path.stat.ST_MTIME] - except os.error, error: - if error.errno != errno.ENOENT or not extension: - raise - # try for a generic template - filename = '_generic.%s'%extension - src = os.path.join(dir, filename) - stime = os.stat(src)[os.path.stat.ST_MTIME] - - key = (dir, filename) - if templates.has_key(key) and stime < templates[key].mtime: - # compiled template is up to date - return templates[key] - - # compile the template - templates[key] = pt = RoundupPageTemplate() - pt.write(open(src).read()) - pt.id = name - pt.mtime = time.time() - return pt +class NoTemplate(Exception): + pass + +class Templates: + templates = {} + + def __init__(self, dir): + self.dir = dir + + def precompileTemplates(self): + ''' Go through a directory and precompile all the templates therein + ''' + for filename in os.listdir(self.dir): + if os.path.isdir(filename): continue + if '.' in filename: + name, extension = filename.split('.') + self.getTemplate(name, extension) + else: + self.getTemplate(filename, None) + + def get(self, name, extension): + ''' Interface to get a template, possibly loading a compiled template. + + "name" and "extension" indicate the template we're after, which in + most cases will be "name.extension". If "extension" is None, then + we look for a template just called "name" with no extension. + + If the file "name.extension" doesn't exist, we look for + "_generic.extension" as a fallback. + ''' + # default the name to "home" + if name is None: + name = 'home' + + # find the source, figure the time it was last modified + if extension: + filename = '%s.%s'%(name, extension) + else: + filename = name + src = os.path.join(self.dir, filename) + try: + stime = os.stat(src)[os.path.stat.ST_MTIME] + except os.error, error: + if error.errno != errno.ENOENT: + raise + if not extension: + raise NoTemplate, 'Template file "%s" doesn\'t exist'%name + + # try for a generic template + generic = '_generic.%s'%extension + src = os.path.join(self.dir, generic) + try: + stime = os.stat(src)[os.path.stat.ST_MTIME] + except os.error, error: + if error.errno != errno.ENOENT: + raise + # nicer error + raise NoTemplate, 'No template file exists for templating '\ + '"%s" with template "%s" (neither "%s" nor "%s")'%(name, + extension, filename, generic) + filename = generic + + if self.templates.has_key(filename) and \ + stime < self.templates[filename].mtime: + # compiled template is up to date + return self.templates[filename] + + # compile the template + self.templates[filename] = pt = RoundupPageTemplate() + pt.write(open(src).read()) + pt.id = filename + pt.mtime = time.time() + return pt + + def __getitem__(self, name): + name, extension = os.path.splitext(name) + if extension: + extension = extension[1:] + try: + return self.get(name, extension) + except NoTemplate, message: + raise KeyError, message class RoundupPageTemplate(PageTemplate.PageTemplate): ''' A Roundup-specific PageTemplate. @@ -128,26 +111,13 @@ class RoundupPageTemplate(PageTemplate.PageTemplate): Interrogate the client to set up the various template variables to be available: - *class* - The current class of node being displayed as an HTMLClass - instance. - *item* - The current node from the database, if we're viewing a specific - node, as an HTMLItem instance. If it doesn't exist, then we're - on a new item page. - (*classname*) - this is one of two things: - - 1. the *item* is also available under its classname, so a *user* - node would also be available under the name *user*. This is - also an HTMLItem instance. - 2. if there's no *item* then the current class is available - through this name, thus "user/name" and "user/name/menu" will - still work - the latter will pull information from the form - if it can. - *form* - The current CGI form information as a mapping of form argument - name to value + *context* + this is one of three things: + 1. None - we're viewing a "home" page + 2. The current class of item being displayed. This is an HTMLClass + instance. + 3. The current item from the database, if we're viewing a specific + item, as an HTMLItem instance. *request* Includes information about the current request, including: - the url @@ -155,33 +125,30 @@ class RoundupPageTemplate(PageTemplate.PageTemplate): ``properties``, etc) parsed out of the form. - methods for easy filterspec link generation - *user*, the current user node as an HTMLItem instance - *instance* - The current instance + - *form*, the current CGI form information as a FieldStorage + *tracker* + The current tracker *db* The current database, through which db.config may be reached. - - Maybe also: - - *modules* - python modules made available (XXX: not sure what's actually in - there tho) ''' def getContext(self, client, classname, request): c = { - 'klass': HTMLClass(client, classname), 'options': {}, 'nothing': None, 'request': request, - 'content': client.content, 'db': HTMLDatabase(client), - 'instance': client.instance + 'tracker': client.instance, + 'utils': TemplatingUtils(client), + 'templates': Templates(client.instance.config.TEMPLATES), } # add in the item if there is one if client.nodeid: - c['item'] = HTMLItem(client.db, classname, client.nodeid) - c[classname] = c['item'] - else: - c[classname] = c['klass'] + if classname == 'user': + c['context'] = HTMLUser(client, classname, client.nodeid) + else: + c['context'] = HTMLItem(client, classname, client.nodeid) + elif client.db.classes.has_key(classname): + c['context'] = HTMLClass(client, classname) return c def render(self, client, classname, request, **options): @@ -193,7 +160,8 @@ class RoundupPageTemplate(PageTemplate.PageTemplate): __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self) if self._v_errors: - raise PTRuntimeError, 'Page Template %s has errors.' % self.id + raise PageTemplate.PTRuntimeError, \ + 'Page Template %s has errors.'%self.id # figure the context classname = classname or client.classname @@ -203,7 +171,7 @@ class RoundupPageTemplate(PageTemplate.PageTemplate): # and go output = StringIO.StringIO() - TALInterpreter(self._v_program, self._v_macros, + TALInterpreter(self._v_program, self.macros, getEngine().getContext(c), output, tal=1, strictinsert=0)() return output.getvalue() @@ -211,51 +179,105 @@ class HTMLDatabase: ''' Return HTMLClasses for valid class fetches ''' def __init__(self, client): - self.client = client + self._client = client + + # we want config to be exposed self.config = client.db.config + + def __getitem__(self, item): + self._client.db.getclass(item) + return HTMLClass(self._client, item) + def __getattr__(self, attr): try: - self.client.db.getclass(attr) + return self[attr] except KeyError: raise AttributeError, attr - return HTMLClass(self.client, attr) + def classes(self): - l = self.client.db.classes.keys() + l = self._client.db.classes.keys() l.sort() - return [HTMLClass(self.client, cn) for cn in l] - -class HTMLClass: + return [HTMLClass(self._client, cn) for cn in l] + +def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')): + cl = db.getclass(prop.classname) + l = [] + for entry in ids: + if num_re.match(entry): + l.append(entry) + else: + l.append(cl.lookup(entry)) + return l + +class HTMLPermissions: + ''' Helpers that provide answers to commonly asked Permission questions. + ''' + def is_edit_ok(self): + ''' Is the user allowed to Edit the current class? + ''' + return self._db.security.hasPermission('Edit', self._client.userid, + self._classname) + def is_view_ok(self): + ''' Is the user allowed to View the current class? + ''' + return self._db.security.hasPermission('View', self._client.userid, + self._classname) + def is_only_view_ok(self): + ''' Is the user only allowed to View (ie. not Edit) the current class? + ''' + return self.is_view_ok() and not self.is_edit_ok() + +class HTMLClass(HTMLPermissions): ''' Accesses through a class (either through *class* or *db.*) ''' def __init__(self, client, classname): - self.client = client - self.db = client.db - self.classname = classname - if classname is not None: - self.klass = self.db.getclass(self.classname) - self.props = self.klass.getprops() + self._client = client + self._db = client.db + + # we want classname to be exposed, but _classname gives a + # consistent API for extending Class/Item + self._classname = self.classname = classname + self._klass = self._db.getclass(self.classname) + self._props = self._klass.getprops() def __repr__(self): return ''%(id(self), self.classname) def __getitem__(self, item): - ''' return an HTMLItem instance''' - #print 'getitem', (self, attr) - if item == 'creator': - return HTMLUser(self.client) + ''' return an HTMLProperty instance + ''' + #print 'HTMLClass.getitem', (self, item) - if not self.props.has_key(item): - raise KeyError, item - prop = self.props[item] + # we don't exist + if item == 'id': + return None + + # get the property + prop = self._props[item] # look up the correct HTMLProperty class + form = self._client.form for klass, htmlklass in propclasses: - if isinstance(prop, hyperdb.Multilink): - value = [] + if not isinstance(prop, klass): + continue + if form.has_key(item): + if isinstance(prop, hyperdb.Multilink): + value = lookupIds(self._db, prop, + handleListCGIValue(form[item])) + elif isinstance(prop, hyperdb.Link): + value = form[item].value.strip() + if value: + value = lookupIds(self._db, prop, [value])[0] + else: + value = None + else: + value = form[item].value.strip() or None else: - value = None - if isinstance(prop, klass): - return htmlklass(self.db, '', prop, item, value) + if isinstance(prop, hyperdb.Multilink): + value = [] + else: + value = None + return htmlklass(self._client, '', prop, item, value) # no good raise KeyError, item @@ -267,22 +289,48 @@ class HTMLClass: except KeyError: raise AttributeError, attr + def getItem(self, itemid, num_re=re.compile('\d+')): + ''' Get an item of this class by its item id. + ''' + # make sure we're looking at an itemid + if not num_re.match(itemid): + itemid = self._klass.lookup(itemid) + + if self.classname == 'user': + klass = HTMLUser + else: + klass = HTMLItem + + return klass(self._client, self.classname, itemid) + def properties(self): - ''' Return HTMLProperty for all props + ''' Return HTMLProperty for all of this class' properties. ''' l = [] - for name, prop in self.props.items(): + for name, prop in self._props.items(): for klass, htmlklass in propclasses: if isinstance(prop, hyperdb.Multilink): value = [] else: value = None if isinstance(prop, klass): - l.append(htmlklass(self.db, '', prop, name, value)) + l.append(htmlklass(self._client, '', prop, name, value)) return l def list(self): - l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()] + ''' List all items in this class. + ''' + if self.classname == 'user': + klass = HTMLUser + else: + klass = HTMLItem + + # get the list and sort it nicely + l = self._klass.list() + sortfunc = make_sort_function(self._db, self.classname) + l.sort(sortfunc) + + l = [klass(self._client, self.classname, x) for x in l] return l def csv(self): @@ -299,23 +347,23 @@ class HTMLClass: p = csv.parser() s = StringIO.StringIO() s.write(p.join(props) + '\n') - for nodeid in self.klass.list(): + for nodeid in self._klass.list(): l = [] for name in props: - value = self.klass.get(nodeid, name) + value = self._klass.get(nodeid, name) if value is None: l.append('') elif isinstance(value, type([])): l.append(':'.join(map(str, value))) else: - l.append(str(self.klass.get(nodeid, name))) + l.append(str(self._klass.get(nodeid, name))) s.write(p.join(l) + '\n') return s.getvalue() def propnames(self): ''' Return the list of the names of the properties of this class. ''' - idlessprops = self.klass.getprops(protected=0).keys() + idlessprops = self._klass.getprops(protected=0).keys() idlessprops.sort() return ['id'] + idlessprops @@ -327,24 +375,35 @@ class HTMLClass: filterspec = request.filterspec sort = request.sort group = request.group - l = [HTMLItem(self.db, self.classname, x) - for x in self.klass.filter(None, filterspec, sort, group)] + if self.classname == 'user': + klass = HTMLUser + else: + klass = HTMLItem + l = [klass(self._client, self.classname, x) + for x in self._klass.filter(None, filterspec, sort, group)] return l - def classhelp(self, properties, label='?', width='400', height='400'): - '''pop up a javascript window with class help + def classhelp(self, properties=None, label='list', width='500', + height='400'): + ''' Pop up a javascript window with class help - This generates a link to a popup window which displays the - properties indicated by "properties" of the class named by - "classname". The "properties" should be a comma-separated list - (eg. 'id,name,description'). + This generates a link to a popup window which displays the + properties indicated by "properties" of the class named by + "classname". The "properties" should be a comma-separated list + (eg. 'id,name,description'). Properties defaults to all the + properties of a class (excluding id, creator, created and + activity). - You may optionally override the label displayed, the width and - height. The popup window will be resizable and scrollable. + You may optionally override the label displayed, the width and + height. The popup window will be resizable and scrollable. ''' + if properties is None: + properties = self._klass.getprops(protected=0).keys() + properties.sort() + properties = ','.join(properties) return '(%s)'%(self.classname, - properties, width, height, label) + 'properties=%s\', \'%s\', \'%s\')">(%s)'%( + self.classname, properties, width, height, label) def submit(self, label="Submit New Entry"): ''' Generate a submit button (and action hidden element) @@ -359,52 +418,51 @@ class HTMLClass: ''' Render this class with the given template. ''' # create a new request and override the specified args - req = HTMLRequest(self.client) + req = HTMLRequest(self._client) req.classname = self.classname req.update(kwargs) # new template, using the specified classname and request - pt = getTemplate(self.db.config.TEMPLATES, self.classname, name) + pt = Templates(self._db.config.TEMPLATES).get(self.classname, name) - # XXX handle PT rendering errors here nicely - try: - # use our fabricated request - return pt.render(self.client, self.classname, req) - except PageTemplate.PTRuntimeError, message: - return '%s
    %s
'%(message, - cgi.escape('
  • '.join(pt._v_errors))) + # use our fabricated request + return pt.render(self._client, self.classname, req) -class HTMLItem: +class HTMLItem(HTMLPermissions): ''' Accesses through an *item* ''' - def __init__(self, db, classname, nodeid): - self.db = db - self.classname = classname - self.nodeid = nodeid - self.klass = self.db.getclass(classname) - self.props = self.klass.getprops() + def __init__(self, client, classname, nodeid): + self._client = client + self._db = client.db + self._classname = classname + self._nodeid = nodeid + self._klass = self._db.getclass(classname) + self._props = self._klass.getprops() def __repr__(self): - return ''%(id(self), self.classname, self.nodeid) + return ''%(id(self), self._classname, + self._nodeid) def __getitem__(self, item): - ''' return an HTMLItem instance''' + ''' return an HTMLProperty instance + ''' + #print 'HTMLItem.getitem', (self, item) if item == 'id': - return self.nodeid - if not self.props.has_key(item): - raise KeyError, item - prop = self.props[item] + return self._nodeid + + # get the property + prop = self._props[item] # get the value, handling missing values - value = self.klass.get(self.nodeid, item, None) + value = self._klass.get(self._nodeid, item, None) if value is None: - if isinstance(self.props[item], hyperdb.Multilink): + if isinstance(self._props[item], hyperdb.Multilink): value = [] # look up the correct HTMLProperty class for klass, htmlklass in propclasses: if isinstance(prop, klass): - return htmlklass(self.db, self.nodeid, prop, item, value) + return htmlklass(self._client, self._nodeid, prop, item, value) raise KeyErorr, item @@ -421,17 +479,24 @@ class HTMLItem: return ' \n'\ ' '%label - # XXX this probably should just return the history items, not the HTML + def journal(self, direction='descending'): + ''' Return a list of HTMLJournalEntry instances. + ''' + # XXX do this + return [] + def history(self, direction='descending'): - l = ['', - '', - _(''), - _(''), - _(''), - _(''), + l = ['
    DateUserActionArgs
    ' + '', + _(''), + _(''), + _(''), + _(''), ''] comments = {} - history = self.klass.history(self.nodeid) + history = self._klass.history(self._nodeid) history.sort() if direction == 'descending': history.reverse() @@ -460,7 +525,7 @@ class HTMLItem: # try to get the relevant property and treat it # specially try: - prop = self.props[k] + prop = self._props[k] except KeyError: prop = None if prop is not None: @@ -469,14 +534,14 @@ class HTMLItem: # figure what the link class is classname = prop.classname try: - linkcl = self.db.getclass(classname) + linkcl = self._db.getclass(classname) except KeyError: labelprop = None comments[classname] = _('''The linked class %(classname)s no longer exists''')%locals() labelprop = linkcl.labelprop(1) hrefable = os.path.exists( - os.path.join(self.db.config.TEMPLATES, + os.path.join(self._db.config.TEMPLATES, classname+'.item')) if isinstance(prop, hyperdb.Multilink) and \ @@ -559,9 +624,8 @@ class HTMLItem: handled by the history display!''') arg_s = '' + str(args) + '' date_s = date_s.replace(' ', ' ') - l.append('' - ''%(date_s, - user, action, arg_s)) + l.append(''%( + date_s, user, action, arg_s)) if comments: l.append(_('')) for entry in comments.values(): @@ -569,20 +633,30 @@ class HTMLItem: l.append('
    ', + _('History'), + '
    DateUserActionArgs
    %s%s%s%s
    %s%s%s%s
    Note:
    ') return '\n'.join(l) - def remove(self): - # XXX do what? - return '' + def renderQueryForm(self): + ''' Render this item, which is a query, as a search form. + ''' + # create a new request and override the specified args + req = HTMLRequest(self._client) + req.classname = self._klass.get(self._nodeid, 'klass') + req.updateFromURL(self._klass.get(self._nodeid, 'url')) + + # new template, using the specified classname and request + pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search') + + # use our fabricated request + return pt.render(self._client, req.classname, req) class HTMLUser(HTMLItem): ''' Accesses through the *user* (a special case of item) ''' - def __init__(self, client): - HTMLItem.__init__(self, client.db, 'user', client.userid) - self.default_classname = client.classname - self.userid = client.userid + def __init__(self, client, classname, nodeid): + HTMLItem.__init__(self, client, 'user', nodeid) + self._default_classname = client.classname # used for security checks - self.security = client.db.security + self._security = client.db.security + _marker = [] def hasPermission(self, role, classname=_marker): ''' Determine if the user has the Role. @@ -591,149 +665,219 @@ class HTMLUser(HTMLItem): be overidden for this test by suppling an alternate classname. ''' if classname is self._marker: - classname = self.default_classname - return self.security.hasPermission(role, self.userid, classname) + classname = self._default_classname + return self._security.hasPermission(role, self._nodeid, classname) + + def is_edit_ok(self): + ''' Is the user allowed to Edit the current class? + Also check whether this is the current user's info. + ''' + return self._db.security.hasPermission('Edit', self._client.userid, + self._classname) or self._nodeid == self._client.userid + + def is_view_ok(self): + ''' Is the user allowed to View the current class? + Also check whether this is the current user's info. + ''' + return self._db.security.hasPermission('Edit', self._client.userid, + self._classname) or self._nodeid == self._client.userid class HTMLProperty: ''' String, Number, Date, Interval HTMLProperty + Has useful attributes: + + _name the name of the property + _value the value of the property if any + A wrapper object which may be stringified for the plain() behaviour. ''' - def __init__(self, db, nodeid, prop, name, value): - self.db = db - self.nodeid = nodeid - self.prop = prop - self.name = name - self.value = value + def __init__(self, client, nodeid, prop, name, value): + self._client = client + self._db = client.db + self._nodeid = nodeid + self._prop = prop + self._name = name + self._value = value def __repr__(self): - return ''%(id(self), self.name, self.prop, self.value) + return ''%(id(self), self._name, self._prop, self._value) def __str__(self): return self.plain() def __cmp__(self, other): if isinstance(other, HTMLProperty): - return cmp(self.value, other.value) - return cmp(self.value, other) + return cmp(self._value, other._value) + return cmp(self._value, other) class StringHTMLProperty(HTMLProperty): def plain(self, escape=0): - if self.value is None: + ''' Render a "plain" representation of the property + ''' + if self._value is None: return '' if escape: - return cgi.escape(str(self.value)) - return str(self.value) + return cgi.escape(str(self._value)) + return str(self._value) def stext(self, escape=0): + ''' Render the value of the property as StructuredText. + + This requires the StructureText module to be installed separately. + ''' s = self.plain(escape=escape) if not StructuredText: return s return StructuredText(s,level=1,header=0) def field(self, size = 30): - if self.value is None: + ''' Render a form edit field for the property + ''' + if self._value is None: value = '' else: - value = cgi.escape(str(self.value)) + value = cgi.escape(str(self._value)) value = '"'.join(value.split('"')) - return ''%(self.name, value, size) + return ''%(self._name, value, size) def multiline(self, escape=0, rows=5, cols=40): - if self.value is None: + ''' Render a multiline form edit field for the property + ''' + if self._value is None: value = '' else: - value = cgi.escape(str(self.value)) + value = cgi.escape(str(self._value)) value = '"'.join(value.split('"')) return ''%( - self.name, rows, cols, value) + self._name, rows, cols, value) def email(self, escape=1): - ''' fudge email ''' - if self.value is None: value = '' - else: value = str(self.value) - value = value.replace('@', ' at ') - value = value.replace('.', ' ') + ''' Render the value of the property as an obscured email address + ''' + if self._value is None: value = '' + else: value = str(self._value) + if value.find('@') != -1: + name, domain = value.split('@') + domain = ' '.join(domain.split('.')[:-1]) + name = name.replace('.', ' ') + value = '%s at %s ...'%(name, domain) + else: + value = value.replace('.', ' ') if escape: value = cgi.escape(value) return value class PasswordHTMLProperty(HTMLProperty): def plain(self): - if self.value is None: + ''' Render a "plain" representation of the property + ''' + if self._value is None: return '' return _('*encrypted*') def field(self, size = 30): - return ''%(self.name, size) + ''' Render a form edit field for the property. + ''' + return ''%(self._name, size) + + def confirm(self, size = 30): + ''' Render a second form edit field for the property, used for + confirmation that the user typed the password correctly. Generates + a field with name "name:confirm". + ''' + return ''%( + self._name, size) class NumberHTMLProperty(HTMLProperty): def plain(self): - return str(self.value) + ''' Render a "plain" representation of the property + ''' + return str(self._value) def field(self, size = 30): - if self.value is None: + ''' Render a form edit field for the property + ''' + if self._value is None: value = '' else: - value = cgi.escape(str(self.value)) + value = cgi.escape(str(self._value)) value = '"'.join(value.split('"')) - return ''%(self.name, value, size) + return ''%(self._name, value, size) class BooleanHTMLProperty(HTMLProperty): def plain(self): + ''' Render a "plain" representation of the property + ''' if self.value is None: return '' - return self.value and "Yes" or "No" + return self._value and "Yes" or "No" def field(self): - checked = self.value and "checked" or "" - s = 'Yes'%(self.name, + ''' Render a form edit field for the property + ''' + checked = self._value and "checked" or "" + s = 'Yes'%(self._name, checked) if checked: checked = "" else: checked = "checked" - s += 'No'%(self.name, + s += 'No'%(self._name, checked) return s class DateHTMLProperty(HTMLProperty): def plain(self): - if self.value is None: + ''' Render a "plain" representation of the property + ''' + if self._value is None: return '' - return str(self.value) + return str(self._value) def field(self, size = 30): - if self.value is None: + ''' Render a form edit field for the property + ''' + if self._value is None: value = '' else: - value = cgi.escape(str(self.value)) + value = cgi.escape(str(self._value)) value = '"'.join(value.split('"')) - return ''%(self.name, value, size) + return ''%(self._name, value, size) def reldate(self, pretty=1): - if not self.value: + ''' Render the interval between the date and now. + + If the "pretty" flag is true, then make the display pretty. + ''' + if not self._value: return '' # figure the interval - interval = date.Date('.') - self.value + interval = date.Date('.') - self._value if pretty: return interval.pretty() return str(interval) class IntervalHTMLProperty(HTMLProperty): def plain(self): - if self.value is None: + ''' Render a "plain" representation of the property + ''' + if self._value is None: return '' - return str(self.value) + return str(self._value) def pretty(self): - return self.value.pretty() + ''' Render the interval in a pretty format (eg. "yesterday") + ''' + return self._value.pretty() def field(self, size = 30): - if self.value is None: + ''' Render a form edit field for the property + ''' + if self._value is None: value = '' else: - value = cgi.escape(str(self.value)) + value = cgi.escape(str(self._value)) value = '"'.join(value.split('"')) - return ''%(self.name, value, size) + return ''%(self._name, value, size) class LinkHTMLProperty(HTMLProperty): ''' Link HTMLProperty @@ -747,84 +891,81 @@ class LinkHTMLProperty(HTMLProperty): ''' def __getattr__(self, attr): ''' return a new HTMLItem ''' - #print 'getattr', (self, attr, self.value) - if not self.value: + #print 'Link.getattr', (self, attr, self._value) + if not self._value: raise AttributeError, "Can't access missing value" - i = HTMLItem(self.db, self.prop.classname, self.value) + if self._prop.classname == 'user': + klass = HTMLUser + else: + klass = HTMLItem + i = klass(self._client, self._prop.classname, self._value) return getattr(i, attr) def plain(self, escape=0): - if self.value is None: - return _('[unselected]') - linkcl = self.db.classes[self.prop.classname] + ''' Render a "plain" representation of the property + ''' + if self._value is None: + return '' + linkcl = self._db.classes[self._prop.classname] k = linkcl.labelprop(1) - value = str(linkcl.get(self.value, k)) + value = str(linkcl.get(self._value, k)) if escape: value = cgi.escape(value) return value - # XXX most of the stuff from here down is of dubious utility - it's easy - # enough to do in the template by hand (and in some cases, it's shorter - # and clearer... - - def field(self): - linkcl = self.db.getclass(self.prop.classname) + def field(self, showid=0, size=None): + ''' Render a form edit field for the property + ''' + linkcl = self._db.getclass(self._prop.classname) if linkcl.getprops().has_key('order'): sort_on = 'order' else: sort_on = linkcl.labelprop() - options = linkcl.filter(None, {}, [sort_on], []) + options = linkcl.filter(None, {}, ('+', sort_on), (None, None)) # TODO: make this a field display, not a menu one! - l = [''%self._name] k = linkcl.labelprop(1) - if value is None: + if self._value is None: s = 'selected ' else: s = '' l.append(_('')%s) for optionid in options: - option = linkcl.get(optionid, k) + # get the option value, and if it's None use an empty string + option = linkcl.get(optionid, k) or '' + + # figure if this option is selected s = '' - if optionid == value: + if optionid == self._value: s = 'selected ' + + # figure the label if showid: - lab = '%s%s: %s'%(self.prop.classname, optionid, option) + lab = '%s%s: %s'%(self._prop.classname, optionid, option) else: lab = option + + # truncate if it's too long if size is not None and len(lab) > size: lab = lab[:size-3] + '...' + + # and generate lab = cgi.escape(lab) l.append(''%(s, optionid, lab)) l.append('') return '\n'.join(l) - def download(self, showid=0): - linkname = self.prop.classname - linkcl = self.db.getclass(linkname) - k = linkcl.labelprop(1) - linkvalue = cgi.escape(str(linkcl.get(self.value, k))) - if showid: - label = value - title = ' title="%s"'%linkvalue - # note ... this should be urllib.quote(linkcl.get(value, k)) - else: - label = linkvalue - title = '' - return '%s'%(linkname, self.value, - linkvalue, title, label) - def menu(self, size=None, height=None, showid=0, additional=[], **conditions): - value = self.value + ''' Render a form select list for this property + ''' + value = self._value # sort function - sortfunc = make_sort_function(self.db, self.prop.classname) + sortfunc = make_sort_function(self._db, self._prop.classname) - # force the value to be a single choice - if isinstance(value, type('')): - value = value[0] - linkcl = self.db.getclass(self.prop.classname) - l = [''%self._name] k = linkcl.labelprop(1) s = '' if value is None: @@ -836,14 +977,21 @@ class LinkHTMLProperty(HTMLProperty): sort_on = ('+', linkcl.labelprop()) options = linkcl.filter(None, conditions, sort_on, (None, None)) for optionid in options: - option = linkcl.get(optionid, k) + # get the option value, and if it's None use an empty string + option = linkcl.get(optionid, k) or '' + + # figure if this option is selected s = '' if value in [optionid, option]: s = 'selected ' + + # figure the label if showid: - lab = '%s%s: %s'%(self.prop.classname, optionid, option) + lab = '%s%s: %s'%(self._prop.classname, optionid, option) else: lab = option + + # truncate if it's too long if size is not None and len(lab) > size: lab = lab[:size-3] + '...' if additional: @@ -851,11 +999,12 @@ class LinkHTMLProperty(HTMLProperty): for propname in additional: m.append(linkcl.get(optionid, propname)) lab = lab + ' (%s)'%', '.join(map(str, m)) + + # and generate lab = cgi.escape(lab) l.append(''%(s, optionid, lab)) l.append('') return '\n'.join(l) - # def checklist(self, ...) class MultilinkHTMLProperty(HTMLProperty): @@ -866,77 +1015,102 @@ class MultilinkHTMLProperty(HTMLProperty): ''' def __len__(self): ''' length of the multilink ''' - return len(self.value) + return len(self._value) def __getattr__(self, attr): ''' no extended attribute accesses make sense here ''' raise AttributeError, attr def __getitem__(self, num): - ''' iterate and return a new HTMLItem ''' - #print 'getitem', (self, num) - value = self.value[num] - return HTMLItem(self.db, self.prop.classname, value) + ''' iterate and return a new HTMLItem + ''' + #print 'Multi.getitem', (self, num) + value = self._value[num] + if self._prop.classname == 'user': + klass = HTMLUser + else: + klass = HTMLItem + return klass(self._client, self._prop.classname, value) + + def __contains__(self, value): + ''' Support the "in" operator + ''' + return value in self._value def reverse(self): - ''' return the list in reverse order ''' - l = self.value[:] + ''' return the list in reverse order + ''' + l = self._value[:] l.reverse() - return [HTMLItem(self.db, self.prop.classname, value) for value in l] + if self._prop.classname == 'user': + klass = HTMLUser + else: + klass = HTMLItem + return [klass(self._client, self._prop.classname, value) for value in l] def plain(self, escape=0): - linkcl = self.db.classes[self.prop.classname] + ''' Render a "plain" representation of the property + ''' + linkcl = self._db.classes[self._prop.classname] k = linkcl.labelprop(1) labels = [] - for v in self.value: + for v in self._value: labels.append(linkcl.get(v, k)) value = ', '.join(labels) if escape: value = cgi.escape(value) return value - # XXX most of the stuff from here down is of dubious utility - it's easy - # enough to do in the template by hand (and in some cases, it's shorter - # and clearer... - def field(self, size=30, showid=0): - sortfunc = make_sort_function(self.db, self.prop.classname) - linkcl = self.db.getclass(self.prop.classname) - value = self.value[:] + ''' Render a form edit field for the property + ''' + sortfunc = make_sort_function(self._db, self._prop.classname) + linkcl = self._db.getclass(self._prop.classname) + value = self._value[:] if value: value.sort(sortfunc) # map the id to the label property + if not linkcl.getkey(): + showid=1 if not showid: k = linkcl.labelprop(1) value = [linkcl.get(v, k) for v in value] value = cgi.escape(','.join(value)) - return ''%(self.name, size, value) + return ''%(self._name, size, value) def menu(self, size=None, height=None, showid=0, additional=[], **conditions): - value = self.value + ''' Render a form select list for this property + ''' + value = self._value # sort function - sortfunc = make_sort_function(self.db, self.prop.classname) + sortfunc = make_sort_function(self._db, self._prop.classname) - linkcl = self.db.getclass(self.prop.classname) + linkcl = self._db.getclass(self._prop.classname) if linkcl.getprops().has_key('order'): sort_on = ('+', 'order') else: sort_on = ('+', linkcl.labelprop()) options = linkcl.filter(None, conditions, sort_on, (None,None)) height = height or min(len(options), 7) - l = [''%(self._name, height)] k = linkcl.labelprop(1) for optionid in options: - option = linkcl.get(optionid, k) + # get the option value, and if it's None use an empty string + option = linkcl.get(optionid, k) or '' + + # figure if this option is selected s = '' if optionid in value or option in value: s = 'selected ' + + # figure the label if showid: - lab = '%s%s: %s'%(self.prop.classname, optionid, option) + lab = '%s%s: %s'%(self._prop.classname, optionid, option) else: lab = option + # truncate if it's too long if size is not None and len(lab) > size: lab = lab[:size-3] + '...' if additional: @@ -944,6 +1118,8 @@ class MultilinkHTMLProperty(HTMLProperty): for propname in additional: m.append(linkcl.get(optionid, propname)) lab = lab + ' (%s)'%', '.join(m) + + # and generate lab = cgi.escape(lab) l.append(''%(s, optionid, lab)) @@ -981,7 +1157,10 @@ def handleListCGIValue(value): if isinstance(value, type([])): return [value.value for value in value] else: - return value.value.split(',') + value = value.value.strip() + if not value: + return [] + return value.split(',') class ShowDict: ''' A convenience access to the :columns index parameters @@ -998,7 +1177,6 @@ class HTMLRequest: "form" the CGI form as a cgi.FieldStorage "env" the CGI environment variables - "url" the current URL path for this request "base" the base URL for this instance "user" a HTMLUser instance for this user "classname" the current classname (possibly None) @@ -1022,13 +1200,17 @@ class HTMLRequest: self.form = client.form self.env = client.env self.base = client.base - self.url = client.url - self.user = HTMLUser(client) + self.user = HTMLUser(client, 'user', client.userid) # store the current class name and action self.classname = client.classname self.template = client.template + self._post_init() + + def _post_init(self): + ''' Set attributes based on self.form + ''' # extract the index display information from the form self.columns = [] if self.form.has_key(':columns'): @@ -1062,15 +1244,17 @@ class HTMLRequest: if self.form.has_key(':filter'): self.filter = handleListCGIValue(self.form[':filter']) self.filterspec = {} + db = self.client.db if self.classname is not None: - props = self.client.db.getclass(self.classname).getprops() + props = db.getclass(self.classname).getprops() for name in self.filter: if self.form.has_key(name): prop = props[name] fv = self.form[name] if (isinstance(prop, hyperdb.Link) or isinstance(prop, hyperdb.Multilink)): - self.filterspec[name] = handleListCGIValue(fv) + self.filterspec[name] = lookupIds(db, prop, + handleListCGIValue(fv)) else: self.filterspec[name] = fv.value @@ -1090,11 +1274,47 @@ class HTMLRequest: else: self.startwith = 0 + def updateFromURL(self, url): + ''' Parse the URL for query args, and update my attributes using the + values. + ''' + self.form = {} + for name, value in cgi.parse_qsl(url): + if self.form.has_key(name): + if isinstance(self.form[name], type([])): + self.form[name].append(cgi.MiniFieldStorage(name, value)) + else: + self.form[name] = [self.form[name], + cgi.MiniFieldStorage(name, value)] + else: + self.form[name] = cgi.MiniFieldStorage(name, value) + self._post_init() + def update(self, kwargs): + ''' Update my attributes using the keyword args + ''' self.__dict__.update(kwargs) if kwargs.has_key('columns'): self.show = ShowDict(self.columns) + def description(self): + ''' Return a description of the request - handle for the page title. + ''' + s = [self.client.db.config.TRACKER_NAME] + if self.classname: + if self.client.nodeid: + s.append('- %s%s'%(self.classname, self.client.nodeid)) + else: + if self.template == 'item': + s.append('- new %s'%self.classname) + elif self.template == 'index': + s.append('- %s index'%self.classname) + else: + s.append('- %s %s'%(self.classname, self.template)) + else: + s.append('- home') + return ' '.join(s) + def __str__(self): d = {} d.update(self.__dict__) @@ -1108,7 +1328,6 @@ class HTMLRequest: d['env'] = e return ''' form: %(form)s -url: %(url)r base: %(base)r classname: %(classname)r template: %(template)r @@ -1152,7 +1371,7 @@ env: %(env)s l.append(s%(':startwith', self.startwith)) return '\n'.join(l) - def indexargs_href(self, url, args): + def indexargs_url(self, url, args): ''' embed the current index args in a URL ''' l = ['%s=%s'%(k,v) for k,v in args.items()] if self.columns and not args.has_key(':columns'): @@ -1181,6 +1400,7 @@ env: %(env)s if not args.has_key(':startwith'): l.append(':startwith=%s'%self.startwith) return '%s?%s'%(url, '&'.join(l)) + indexargs_href = indexargs_url def base_javascript(self): return ''' @@ -1196,7 +1416,7 @@ function submit_once() { } function help_window(helpurl, width, height) { - HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width); + HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width); } '''%self.base @@ -1217,20 +1437,45 @@ function help_window(helpurl, width, height) { matches = None l = klass.filter(matches, filterspec, sort, group) - # return the batch object - return Batch(self.client, self.classname, l, self.pagesize, - self.startwith) - + # return the batch object, using IDs only + return Batch(self.client, l, self.pagesize, self.startwith, + classname=self.classname) # extend the standard ZTUtils Batch object to remove dependency on # Acquisition and add a couple of useful methods class Batch(ZTUtils.Batch): - def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0): + ''' Use me to turn a list of items, or item ids of a given class, into a + series of batches. + + ========= ======================================================== + Parameter Usage + ========= ======================================================== + sequence a list of HTMLItems or item ids + classname if sequence is a list of ids, this is the class of item + size how big to make the sequence. + start where to start (0-indexed) in the sequence. + end where to end (0-indexed) in the sequence. + orphan if the next batch would contain less items than this + value, then it is combined with this batch + overlap the number of items shared between adjacent batches + ========= ======================================================== + + Attributes: Note that the "start" attribute, unlike the + argument, is a 1-based index (I know, lame). "first" is the + 0-based index. "length" is the actual number of elements in + the batch. + + "sequence_length" is the length of the original, unbatched, sequence. + ''' + def __init__(self, client, sequence, size, start, end=0, orphan=0, + overlap=0, classname=None): self.client = client - self.classname = classname self.last_index = self.last_item = None self.current_item = None - ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap) + self.classname = classname + self.sequence_length = len(sequence) + ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan, + overlap) # overwrite so we can late-instantiate the HTMLItem instance def __getitem__(self, index): @@ -1238,7 +1483,8 @@ class Batch(ZTUtils.Batch): if index + self.end < self.first: raise IndexError, index return self._sequence[index + self.end] - if index >= self.length: raise IndexError, index + if index >= self.length: + raise IndexError, index # move the last_item along - but only if the fetched index changes # (for some reason, index 0 is fetched twice) @@ -1246,10 +1492,15 @@ class Batch(ZTUtils.Batch): self.last_item = self.current_item self.last_index = index - # wrap the return in an HTMLItem - self.current_item = HTMLItem(self.client.db, self.classname, - self._sequence[index+self.first]) - return self.current_item + item = self._sequence[index + self.first] + if self.classname: + # map the item ids to instances + if self.classname == 'user': + item = HTMLUser(self.client, self.classname, item) + else: + item = HTMLItem(self.client, self.classname, item) + self.current_item = item + return item def propchanged(self, property): ''' Detect if the property marked as being the group property @@ -1264,7 +1515,7 @@ class Batch(ZTUtils.Batch): def previous(self): if self.start == 1: return None - return Batch(self.client, self.classname, self._sequence, self._size, + return Batch(self.client, self._sequence, self._size, self.first - self._size + self.overlap, 0, self.orphan, self.overlap) @@ -1273,10 +1524,15 @@ class Batch(ZTUtils.Batch): self._sequence[self.end] except IndexError: return None - return Batch(self.client, self.classname, self._sequence, self._size, + return Batch(self.client, self._sequence, self._size, self.end - self.overlap, 0, self.orphan, self.overlap) - def length(self): - self.sequence_length = l = len(self._sequence) - return l +class TemplatingUtils: + ''' Utilities for templating + ''' + def __init__(self, client): + self.client = client + def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0): + return Batch(self.client, sequence, size, start, end, orphan, + overlap)