X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fcgi%2Ftemplating.py;h=a403481864364642d7a03072f332ee334593f6d9;hb=a26e8ef75040dc5f5203680378b56dc0e92ede98;hp=dc8482639db6cb89ae14e0bc38438776989c6691;hpb=bcfe31824e52e299776736a89b8ac88ad66bd9c4;p=roundup.git diff --git a/roundup/cgi/templating.py b/roundup/cgi/templating.py index dc84826..a403481 100644 --- a/roundup/cgi/templating.py +++ b/roundup/cgi/templating.py @@ -1,6 +1,12 @@ -import sys, cgi, urllib, os, re, os.path, time, errno +"""Implements the API used in the HTML templating for the web interface. +""" +__docformat__ = 'restructuredtext' -from roundup import hyperdb, date +from __future__ import nested_scopes + +import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes + +from roundup import hyperdb, date, rcsv from roundup.i18n import _ try: @@ -25,6 +31,14 @@ from roundup.cgi import ZTUtils class NoTemplate(Exception): pass +class Unauthorised(Exception): + def __init__(self, action, klass): + self.action = action + self.klass = klass + def __str__(self): + return 'You are not allowed to %s items of class %s'%(self.action, + self.klass) + def find_template(dir, name, extension): ''' Find a template in the nominated dir ''' @@ -116,7 +130,9 @@ class Templates: # compile the template self.templates[src] = pt = RoundupPageTemplate() - pt.write(open(src).read()) + # use pt_edit so we can pass the content_type guess too + content_type = mimetypes.guess_type(filename)[0] or 'text/html' + pt.pt_edit(open(src).read(), content_type) pt.id = filename pt.mtime = time.time() return pt @@ -131,35 +147,37 @@ class Templates: raise KeyError, message class RoundupPageTemplate(PageTemplate.PageTemplate): - ''' A Roundup-specific PageTemplate. - - Interrogate the client to set up the various template variables to - be available: - - *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 - - the current index information (``filterspec``, ``filter`` args, - ``properties``, etc) parsed out of the form. - - methods for easy filterspec link generation - - *user*, the current user node as an HTMLItem instance - - *form*, the current CGI form information as a FieldStorage - *config* - The current tracker config. - *db* - The current database, used to access arbitrary database items. - *utils* - This is a special class that has its base in the TemplatingUtils - class in this file. If the tracker interfaces module defines a - TemplatingUtils class then it is mixed in, overriding the methods - in the base class. + '''A Roundup-specific PageTemplate. + + Interrogate the client to set up the various template variables to + be available: + + *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 + - the current index information (``filterspec``, ``filter`` args, + ``properties``, etc) parsed out of the form. + - methods for easy filterspec link generation + - *user*, the current user node as an HTMLItem instance + - *form*, the current CGI form information as a FieldStorage + *config* + The current tracker config. + *db* + The current database, used to access arbitrary database items. + *utils* + This is a special class that has its base in the TemplatingUtils + class in this file. If the tracker interfaces module defines a + TemplatingUtils class then it is mixed in, overriding the methods + in the base class. ''' def getContext(self, client, classname, request): # construct the TemplatingUtils class @@ -187,7 +205,10 @@ class RoundupPageTemplate(PageTemplate.PageTemplate): c['context'] = HTMLItem(client, classname, client.nodeid, anonymous=1) elif client.db.classes.has_key(classname): - c['context'] = HTMLClass(client, classname, anonymous=1) + if classname == 'user': + c['context'] = HTMLUserClass(client, classname, anonymous=1) + else: + c['context'] = HTMLClass(client, classname, anonymous=1) return c def render(self, client, classname, request, **options): @@ -214,6 +235,9 @@ class RoundupPageTemplate(PageTemplate.PageTemplate): getEngine().getContext(c), output, tal=1, strictinsert=0)() return output.getvalue() + def __repr__(self): + return ''%self.id + class HTMLDatabase: ''' Return HTMLClasses for valid class fetches ''' @@ -232,6 +256,8 @@ class HTMLDatabase: return HTMLItem(self._client, m.group('cl'), m.group('id')) else: self._client.db.getclass(item) + if item == 'user': + return HTMLUserClass(self._client, item) return HTMLClass(self._client, item) def __getattr__(self, attr): @@ -243,9 +269,17 @@ class HTMLDatabase: def classes(self): l = self._client.db.classes.keys() l.sort() - return [HTMLClass(self._client, cn) for cn in l] - -def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')): + r = [] + for item in l: + if item == 'user': + m.append(HTMLUserClass(self._client, item)) + m.append(HTMLClass(self._client, item)) + return r + +def lookupIds(db, prop, ids, fail_ok=0, num_re=re.compile('-?\d+')): + ''' "fail_ok" should be specified if we wish to pass through bad values + (most likely form values that we wish to represent back to the user) + ''' cl = db.getclass(prop.classname) l = [] for entry in ids: @@ -254,9 +288,22 @@ def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')): else: try: l.append(cl.lookup(entry)) - except KeyError: - # ignore invalid keys - pass + except (TypeError, KeyError): + if fail_ok: + # pass through the bad value + l.append(entry) + return l + +def lookupKeys(linkcl, key, ids, num_re=re.compile('-?\d+')): + ''' Look up the "key" values for "ids" list - though some may already + be key values, not ids. + ''' + l = [] + for entry in ids: + if num_re.match(entry): + l.append(linkcl.get(entry, key)) + else: + l.append(entry) return l class HTMLPermissions: @@ -267,17 +314,52 @@ class HTMLPermissions: ''' 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): + def view_check(self): + ''' Raise the Unauthorised exception if the user's not permitted to + view this class. + ''' + if not self.is_view_ok(): + raise Unauthorised("view", self._classname) + + def edit_check(self): + ''' Raise the Unauthorised exception if the user's not permitted to + edit this class. + ''' + if not self.is_edit_ok(): + raise Unauthorised("edit", self._classname) + +def input_html4(**attrs): + """Generate an 'input' (html4) element with given attributes""" + return ''%' '.join(['%s="%s"'%item for item in attrs.items()]) + +def input_xhtml(**attrs): + """Generate an 'input' (xhtml) element with given attributes""" + return ''%' '.join(['%s="%s"'%item for item in attrs.items()]) + +class HTMLInputMixin: + ''' requires a _client property ''' + def __init__(self): + html_version = 'html4' + if hasattr(self._client.instance.config, 'HTML_VERSION'): + html_version = self._client.instance.config.HTML_VERSION + if html_version == 'xhtml': + self.input = input_xhtml + else: + self.input = input_html4 + +class HTMLClass(HTMLInputMixin, HTMLPermissions): ''' Accesses through a class (either through *class* or *db.*) ''' def __init__(self, client, classname, anonymous=0): @@ -291,6 +373,8 @@ class HTMLClass(HTMLPermissions): self._klass = self._db.getclass(self.classname) self._props = self._klass.getprops() + HTMLInputMixin.__init__(self) + def __repr__(self): return ''%(id(self), self.classname) @@ -304,7 +388,10 @@ class HTMLClass(HTMLPermissions): return None # get the property - prop = self._props[item] + try: + prop = self._props[item] + except KeyError: + raise KeyError, 'No such property "%s" on %s'%(item, self.classname) # look up the correct HTMLProperty class form = self._client.form @@ -314,11 +401,12 @@ class HTMLClass(HTMLPermissions): if form.has_key(item): if isinstance(prop, hyperdb.Multilink): value = lookupIds(self._db, prop, - handleListCGIValue(form[item])) + handleListCGIValue(form[item]), fail_ok=1) elif isinstance(prop, hyperdb.Link): value = form[item].value.strip() if value: - value = lookupIds(self._db, prop, [value])[0] + value = lookupIds(self._db, prop, [value], + fail_ok=1)[0] else: value = None else: @@ -341,11 +429,15 @@ class HTMLClass(HTMLPermissions): except KeyError: raise AttributeError, attr - def getItem(self, itemid, num_re=re.compile('\d+')): + def designator(self): + ''' Return this class' designator (classname) ''' + return self._classname + + 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): + if not isinstance(itemid, type(1)) and not num_re.match(itemid): itemid = self._klass.lookup(itemid) if self.classname == 'user': @@ -372,7 +464,7 @@ class HTMLClass(HTMLPermissions): l.sort(lambda a,b:cmp(a._name, b._name)) return l - def list(self): + def list(self, sort_on=None): ''' List all items in this class. ''' if self.classname == 'user': @@ -382,7 +474,7 @@ class HTMLClass(HTMLPermissions): # get the list and sort it nicely l = self._klass.list() - sortfunc = make_sort_function(self._db, self.classname) + sortfunc = make_sort_function(self._db, self.classname, sort_on) l.sort(sortfunc) l = [klass(self._client, self.classname, x) for x in l] @@ -391,17 +483,13 @@ class HTMLClass(HTMLPermissions): def csv(self): ''' Return the items of this class as a chunk of CSV text. ''' - # get the CSV module - try: - import csv - except ImportError: - return 'Sorry, you need the csv module to use this function.\n'\ - 'Get it from: http://www.object-craft.com.au/projects/csv/' + if rcsv.error: + return rcsv.error props = self.propnames() - p = csv.parser() s = StringIO.StringIO() - s.write(p.join(props) + '\n') + writer = rcsv.writer(s, rcsv.comma_separated) + writer.writerow(props) for nodeid in self._klass.list(): l = [] for name in props: @@ -412,7 +500,7 @@ class HTMLClass(HTMLPermissions): l.append(':'.join(map(str, value))) else: l.append(str(self._klass.get(nodeid, name))) - s.write(p.join(l) + '\n') + writer.writerow(l) return s.getvalue() def propnames(self): @@ -422,19 +510,17 @@ class HTMLClass(HTMLPermissions): idlessprops.sort() return ['id'] + idlessprops - def filter(self, request=None): + def filter(self, request=None, filterspec={}, sort=(None,None), + group=(None,None)): ''' Return a list of items from this class, filtered and sorted by the current requested filterspec/filter/sort/group args + + "request" takes precedence over the other three arguments. ''' - # XXX allow direct specification of the filterspec etc. if request is not None: filterspec = request.filterspec sort = request.sort group = request.group - else: - filterspec = {} - sort = (None,None) - group = (None,None) if self.classname == 'user': klass = HTMLUser else: @@ -465,19 +551,23 @@ class HTMLClass(HTMLPermissions): properties.sort() properties = ','.join(properties) if property: - property = '&property=%s'%property + property = '&property=%s'%property return '%s'%(self.classname, properties, property, width, height, label) def submit(self, label="Submit New Entry"): ''' Generate a submit button (and action hidden element) ''' - return ' \n'\ - ' '%label + self.view_check() + if self.is_edit_ok(): + return self.input(type="hidden",name="@action",value="new") + \ + '\n' + self.input(type="submit",name="submit",value=label) + return '' def history(self): + self.view_check() return 'New node - no history' def renderWith(self, name, **kwargs): @@ -492,9 +582,13 @@ class HTMLClass(HTMLPermissions): pt = Templates(self._db.config.TEMPLATES).get(self.classname, name) # use our fabricated request - return pt.render(self._client, self.classname, req) + args = { + 'ok_message': self._client.ok_message, + 'error_message': self._client.error_message + } + return pt.render(self._client, self.classname, req, **args) -class HTMLItem(HTMLPermissions): +class HTMLItem(HTMLInputMixin, HTMLPermissions): ''' Accesses through an *item* ''' def __init__(self, client, classname, nodeid, anonymous=0): @@ -508,6 +602,8 @@ class HTMLItem(HTMLPermissions): # do we prefix the form items with the item's identification? self._anonymous = anonymous + HTMLInputMixin.__init__(self) + def __repr__(self): return ''%(id(self), self._classname, self._nodeid) @@ -544,12 +640,19 @@ class HTMLItem(HTMLPermissions): return self[attr] except KeyError: raise AttributeError, attr + + def designator(self): + """Return this item's designator (classname + id).""" + return '%s%s'%(self._classname, self._nodeid) def submit(self, label="Submit Changes"): - ''' Generate a submit button (and action hidden element) - ''' - return ' \n'\ - ' '%label + """Generate a submit button. + + Also sneak in the lastactivity and action hidden elements. + """ + return self.input(type="hidden", name="@lastactivity", value=date.Date('.')) + '\n' + \ + self.input(type="hidden", name="@action", value="edit") + '\n' + \ + self.input(type="submit", name="submit", value=label) def journal(self, direction='descending'): ''' Return a list of HTMLJournalEntry instances. @@ -558,6 +661,8 @@ class HTMLItem(HTMLPermissions): return [] def history(self, direction='descending', dre=re.compile('\d+')): + self.view_check() + l = ['' '
', _('History'), @@ -583,8 +688,10 @@ class HTMLItem(HTMLPermissions): isinstance(self._props[prop_n], hyperdb.Link)): classname = self._props[prop_n].classname try: - find_template(self._db.config.TEMPLATES, + template = find_template(self._db.config.TEMPLATES, classname, 'item') + if template[1].startswith('_generic'): + raise NoTemplate, 'not really...' except NoTemplate: pass else: @@ -620,116 +727,123 @@ class HTMLItem(HTMLPermissions): prop = self._props[k] except KeyError: 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.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, - classname+'.item')) - - if isinstance(prop, hyperdb.Multilink) and args[k]: - ml = [] - for linkid in args[k]: - if isinstance(linkid, type(())): - sublabel = linkid[0] + ' ' - linkids = linkid[1] - else: - sublabel = '' - linkids = [linkid] - subml = [] - for linkid in linkids: - 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 and \ - labelprop != 'id': - label = linkcl.get(linkid, labelprop) - except IndexError: - comments['no_link'] = _('''The - linked node no longer - exists''') - subml.append('%s'%label) - else: - if hrefable: - subml.append('%s'%( - classname, linkid, label)) - else: - subml.append(label) - ml.append(sublabel + ', '.join(subml)) - cell.append('%s:\n %s'%(k, ', '.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 and labelprop != 'id': + if prop is None: + # property no longer exists + comments['no_exist'] = _('''The indicated property + no longer exists''') + cell.append('%s: %s\n'%(k, str(args[k]))) + continue + + 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.getclass(classname) + except KeyError: + labelprop = None + comments[classname] = _('''The linked class + %(classname)s no longer exists''')%locals() + labelprop = linkcl.labelprop(1) + try: + template = find_template(self._db.config.TEMPLATES, + classname, 'item') + if template[1].startswith('_generic'): + raise NoTemplate, 'not really...' + hrefable = 1 + except NoTemplate: + hrefable = 0 + + if isinstance(prop, hyperdb.Multilink) and args[k]: + ml = [] + for linkid in args[k]: + if isinstance(linkid, type(())): + sublabel = linkid[0] + ' ' + linkids = linkid[1] + else: + sublabel = '' + linkids = [linkid] + subml = [] + for linkid in linkids: + 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: - label = linkcl.get(args[k], labelprop) + if labelprop is not None and \ + labelprop != 'id': + label = linkcl.get(linkid, 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: - if hrefable: - old = '%s'%(classname, args[k], label) + subml.append('%s'%label) else: - old = label; - cell.append('%s: %s' % (k,old)) - if current.has_key(k): - cell[-1] += ' -> %s'%current[k] - current[k] = old - - elif isinstance(prop, hyperdb.Date) and args[k]: - d = date.Date(args[k]).local(timezone) - cell.append('%s: %s'%(k, str(d))) - if current.has_key(k): - cell[-1] += ' -> %s' % current[k] - current[k] = str(d) - - elif isinstance(prop, hyperdb.Interval) and args[k]: - d = date.Interval(args[k]) - cell.append('%s: %s'%(k, str(d))) - if current.has_key(k): - cell[-1] += ' -> %s'%current[k] - current[k] = str(d) - - elif isinstance(prop, hyperdb.String) and args[k]: - cell.append('%s: %s'%(k, cgi.escape(args[k]))) - if current.has_key(k): - cell[-1] += ' -> %s'%current[k] - current[k] = cgi.escape(args[k]) - - elif not args[k]: - if current.has_key(k): - cell.append('%s: %s'%(k, current[k])) - current[k] = '(no value)' + if hrefable: + subml.append('%s'%( + classname, linkid, label)) + else: + subml.append(label) + ml.append(sublabel + ', '.join(subml)) + cell.append('%s:\n %s'%(k, ', '.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 and labelprop != 'id': + 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: + if hrefable: + old = '%s'%(classname, args[k], label) else: - cell.append('%s: (no value)'%k) - - else: - cell.append('%s: %s'%(k, str(args[k]))) + old = label; + cell.append('%s: %s' % (k,old)) if current.has_key(k): cell[-1] += ' -> %s'%current[k] - current[k] = str(args[k]) + current[k] = old + + elif isinstance(prop, hyperdb.Date) and args[k]: + d = date.Date(args[k]).local(timezone) + cell.append('%s: %s'%(k, str(d))) + if current.has_key(k): + cell[-1] += ' -> %s' % current[k] + current[k] = str(d) + + elif isinstance(prop, hyperdb.Interval) and args[k]: + d = date.Interval(args[k]) + cell.append('%s: %s'%(k, str(d))) + if current.has_key(k): + cell[-1] += ' -> %s'%current[k] + current[k] = str(d) + + elif isinstance(prop, hyperdb.String) and args[k]: + cell.append('%s: %s'%(k, cgi.escape(args[k]))) + if current.has_key(k): + cell[-1] += ' -> %s'%current[k] + current[k] = cgi.escape(args[k]) + + elif not args[k]: + if current.has_key(k): + cell.append('%s: %s'%(k, current[k])) + current[k] = '(no value)' + else: + cell.append('%s: (no value)'%k) + else: - # property no longer exists - comments['no_exist'] = _('''The indicated property - no longer exists''') - cell.append('%s: %s\n'%(k, str(args[k]))) + cell.append('%s: %s'%(k, str(args[k]))) + if current.has_key(k): + cell[-1] += ' -> %s'%current[k] + current[k] = str(args[k]) + arg_s = '
'.join(cell) else: # unkown event!! @@ -758,7 +872,7 @@ class HTMLItem(HTMLPermissions): req.classname = self._klass.get(self._nodeid, 'klass') name = self._klass.get(self._nodeid, 'name') req.updateFromURL(self._klass.get(self._nodeid, 'url') + - '&:queryname=%s'%urllib.quote(name)) + '&@queryname=%s'%urllib.quote(name)) # new template, using the specified classname and request pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search') @@ -766,7 +880,44 @@ class HTMLItem(HTMLPermissions): # use our fabricated request return pt.render(self._client, req.classname, req) -class HTMLUser(HTMLItem): +class HTMLUserPermission: + + 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._user_perm_check('Edit') + + 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._user_perm_check('View') + + def _user_perm_check(self, type): + # some users may view / edit all users + s = self._db.security + userid = self._client.userid + if s.hasPermission(type, userid, self._classname): + return 1 + + # users may view their own info + is_anonymous = self._db.user.get(userid, 'username') == 'anonymous' + if getattr(self, '_nodeid', None) == userid and not is_anonymous: + return 1 + + # may anonymous users register? + if (is_anonymous and s.hasPermission('Web Registration', userid, + self._classname)): + return 1 + + # nope, no access here + return 0 + +class HTMLUserClass(HTMLUserPermission, HTMLClass): + pass + +class HTMLUser(HTMLUserPermission, HTMLItem): ''' Accesses through the *user* (a special case of item) ''' def __init__(self, client, classname, nodeid, anonymous=0): @@ -787,21 +938,7 @@ class HTMLUser(HTMLItem): classname = self._default_classname return self._security.hasPermission(permission, 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: +class HTMLProperty(HTMLInputMixin, HTMLPermissions): ''' String, Number, Date, Interval HTMLProperty Has useful attributes: @@ -825,6 +962,9 @@ class HTMLProperty: self._formname = '%s%s@%s'%(classname, nodeid, name) else: self._formname = name + + HTMLInputMixin.__init__(self) + def __repr__(self): return ''%(id(self), self._formname, self._prop, self._value) @@ -835,6 +975,26 @@ class HTMLProperty: return cmp(self._value, other._value) return cmp(self._value, other) + def is_edit_ok(self): + ''' Is the user allowed to Edit the current class? + ''' + thing = HTMLDatabase(self._client)[self._classname] + if self._nodeid: + # this is a special-case for the User class where permission's + # on a per-item basis :( + thing = thing.getItem(self._nodeid) + return thing.is_edit_ok() + + def is_view_ok(self): + ''' Is the user allowed to View the current class? + ''' + thing = HTMLDatabase(self._client)[self._classname] + if self._nodeid: + # this is a special-case for the User class where permission's + # on a per-item basis :( + thing = thing.getItem(self._nodeid) + return thing.is_view_ok() + class StringHTMLProperty(HTMLProperty): hyper_re = re.compile(r'((?P\w{3,6}://\S+)|' r'(?P[-+=%/\w\.]+@[\w\.\-]+)|' @@ -852,18 +1012,26 @@ class StringHTMLProperty(HTMLProperty): s2 = match.group('id') try: # make sure s1 is a valid tracker classname - self._db.getclass(s1) - return '%s %s'%(s, s1, s2) + cl = self._db.getclass(s1) + if not cl.hasnode(s2): + raise KeyError, 'oops' + return '%s%s'%(s, s1, s2) except KeyError: return '%s%s'%(s1, s2) + def hyperlinked(self): + ''' Render a "hyperlinked" version of the text ''' + return self.plain(hyperlink=1) + def plain(self, escape=0, hyperlink=0): - ''' Render a "plain" representation of the property + '''Render a "plain" representation of the property - "escape" turns on/off HTML quoting - "hyperlink" turns on/off in-text hyperlinking of URLs, email - addresses and designators + - "escape" turns on/off HTML quoting + - "hyperlink" turns on/off in-text hyperlinking of URLs, email + addresses and designators ''' + self.view_check() + if self._value is None: return '' if escape: @@ -871,6 +1039,7 @@ class StringHTMLProperty(HTMLProperty): else: s = str(self._value) if hyperlink: + # no, we *must* escape this text if not escape: s = cgi.escape(s) s = self.hyper_re.sub(self._hyper_repl, s) @@ -881,37 +1050,59 @@ class StringHTMLProperty(HTMLProperty): This requires the StructureText module to be installed separately. ''' + self.view_check() + s = self.plain(escape=escape) if not StructuredText: return s return StructuredText(s,level=1,header=0) def field(self, size = 30): - ''' Render a form edit field for the property + ''' Render the property as a field in HTML. + + If not editable, just display the value via plain(). ''' + self.view_check() + if self._value is None: value = '' else: value = cgi.escape(str(self._value)) + + if self.is_edit_ok(): value = '"'.join(value.split('"')) - return ''%(self._formname, value, size) + return self.input(name=self._formname,value=value,size=size) + + return self.plain() def multiline(self, escape=0, rows=5, cols=40): - ''' Render a multiline form edit field for the property + ''' Render a multiline form edit field for the property. + + If not editable, just display the plain() value in a
 tag.
         '''
+        self.view_check()
+
         if self._value is None:
             value = ''
         else:
             value = cgi.escape(str(self._value))
+
+        if self.is_edit_ok():
             value = '"'.join(value.split('"'))
-        return ''%(
-            self._formname, rows, cols, value)
+            return ''%(
+                self._formname, rows, cols, value)
+
+        return '
%s
'%self.plain() def email(self, escape=1): ''' Render the value of the property as an obscured email address ''' - if self._value is None: value = '' - else: value = str(self._value) + self.view_check() + + if self._value is None: + value = '' + else: + value = str(self._value) if value.find('@') != -1: name, domain = value.split('@') domain = ' '.join(domain.split('.')[:-1]) @@ -927,38 +1118,64 @@ class PasswordHTMLProperty(HTMLProperty): def plain(self): ''' Render a "plain" representation of the property ''' + self.view_check() + if self._value is None: return '' return _('*encrypted*') def field(self, size = 30): ''' Render a form edit field for the property. + + If not editable, just display the value via plain(). ''' - return ''%(self._formname, size) + self.view_check() + + if self.is_edit_ok(): + return self.input(type="password", name=self._formname, size=size) + + return self.plain() 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 ":confirm:name". + a field with name "@confirm@name". + + If not editable, display nothing. ''' - return ''%( - self._formname, size) + self.view_check() + + if self.is_edit_ok(): + return self.input(type="password", + name="@confirm@%s"%self._formname, size=size) + + return '' class NumberHTMLProperty(HTMLProperty): def plain(self): ''' Render a "plain" representation of the property ''' + self.view_check() + return str(self._value) def field(self, size = 30): - ''' Render a form edit field for the property + ''' Render a form edit field for the property. + + If not editable, just display the value via plain(). ''' + self.view_check() + if self._value is None: value = '' else: value = cgi.escape(str(self._value)) + + if self.is_edit_ok(): value = '"'.join(value.split('"')) - return ''%(self._formname, value, size) + return self.input(name=self._formname,value=value,size=size) + + return self.plain() def __int__(self): ''' Return an int of me @@ -975,28 +1192,43 @@ class BooleanHTMLProperty(HTMLProperty): def plain(self): ''' Render a "plain" representation of the property ''' + self.view_check() + if self._value is None: return '' return self._value and "Yes" or "No" def field(self): ''' Render a form edit field for the property + + If not editable, just display the value via plain(). ''' + self.view_check() + + if not is_edit_ok(): + return self.plain() + checked = self._value and "checked" or "" - s = 'Yes'%(self._formname, - checked) - if checked: - checked = "" + if self._value: + s = self.input(type="radio", name=self._formname, value="yes", + checked="checked") + s += 'Yes' + s +=self.input(type="radio", name=self._formname, value="no") + s += 'No' else: - checked = "checked" - s += 'No'%(self._formname, - checked) + s = self.input(type="radio", name=self._formname, value="yes") + s += 'Yes' + s +=self.input(type="radio", name=self._formname, value="no", + checked="checked") + s += 'No' return s class DateHTMLProperty(HTMLProperty): def plain(self): ''' Render a "plain" representation of the property ''' + self.view_check() + if self._value is None: return '' return str(self._value.local(self._db.getUserTimezone())) @@ -1007,29 +1239,42 @@ class DateHTMLProperty(HTMLProperty): This is useful for defaulting a new value. Returns a DateHTMLProperty. ''' - return DateHTMLProperty(self._client, self._nodeid, self._prop, - self._formname, date.Date('.')) + self.view_check() + + return DateHTMLProperty(self._client, self._classname, self._nodeid, + self._prop, self._formname, date.Date('.')) def field(self, size = 30): ''' Render a form edit field for the property + + If not editable, just display the value via plain(). ''' + self.view_check() + if self._value is None: value = '' else: - value = cgi.escape(str(self._value.local(self._db.getUserTimezone()))) + tz = self._db.getUserTimezone() + value = cgi.escape(str(self._value.local(tz))) + + if is_edit_ok(): value = '"'.join(value.split('"')) - return ''%(self._formname, value, size) + return self.input(name=self._formname,value=value,size=size) + + return self.plain() def reldate(self, pretty=1): ''' Render the interval between the date and now. If the "pretty" flag is true, then make the display pretty. ''' + self.view_check() + if not self._value: return '' # figure the interval - interval = date.Date('.') - self._value + interval = self._value - date.Date('.') if pretty: return interval.pretty() return str(interval) @@ -1043,6 +1288,8 @@ class DateHTMLProperty(HTMLProperty): string, then it'll be stripped from the output. This is handy for the situatin when a date only specifies a month and a year. ''' + self.view_check() + if format is not self._marker: return self._value.pretty(format) else: @@ -1051,13 +1298,17 @@ class DateHTMLProperty(HTMLProperty): def local(self, offset): ''' Return the date/time as a local (timezone offset) date/time. ''' - return DateHTMLProperty(self._client, self._nodeid, self._prop, - self._formname, self._value.local(offset)) + self.view_check() + + return DateHTMLProperty(self._client, self._classname, self._nodeid, + self._prop, self._formname, self._value.local(offset)) class IntervalHTMLProperty(HTMLProperty): def plain(self): ''' Render a "plain" representation of the property ''' + self.view_check() + if self._value is None: return '' return str(self._value) @@ -1065,17 +1316,27 @@ class IntervalHTMLProperty(HTMLProperty): def pretty(self): ''' Render the interval in a pretty format (eg. "yesterday") ''' + self.view_check() + return self._value.pretty() def field(self, size = 30): ''' Render a form edit field for the property + + If not editable, just display the value via plain(). ''' + self.view_check() + if self._value is None: value = '' else: value = cgi.escape(str(self._value)) + + if is_edit_ok(): value = '"'.join(value.split('"')) - return ''%(self._formname, value, size) + return self.input(name=self._formname,value=value,size=size) + + return self.plain() class LinkHTMLProperty(HTMLProperty): ''' Link HTMLProperty @@ -1109,6 +1370,8 @@ class LinkHTMLProperty(HTMLProperty): def plain(self, escape=0): ''' Render a "plain" representation of the property ''' + self.view_check() + if self._value is None: return '' linkcl = self._db.classes[self._prop.classname] @@ -1120,71 +1383,56 @@ class LinkHTMLProperty(HTMLProperty): def field(self, showid=0, size=None): ''' Render a form edit field for the property + + If not editable, just display the value via plain(). ''' + self.view_check() + + if not self.is_edit_ok(): + return self.plain() + + # edit field 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), (None, None)) - # TODO: make this a field display, not a menu one! - l = ['') - return '\n'.join(l) + label = self._value + value = cgi.escape(str(self._value)) + value = '"'.join(value.split('"')) + return ''%(self._formname, + label, size) def menu(self, size=None, height=None, showid=0, additional=[], - **conditions): + sort_on=None, **conditions): ''' Render a form select list for this property + + If not editable, just display the value via plain(). ''' - value = self._value + self.view_check() + + if not self.is_edit_ok(): + return self.plain() - # sort function - sortfunc = make_sort_function(self._db, self._prop.classname) + value = self._value linkcl = self._db.getclass(self._prop.classname) l = [''%(self._formname, size, value) + return self.input(name=self._formname,size=size,value=value) def menu(self, size=None, height=None, showid=0, additional=[], - **conditions): + sort_on=None, **conditions): ''' Render a form select list for this property + + If not editable, just display the value via plain(). ''' - value = self._value + self.view_check() - # sort function - sortfunc = make_sort_function(self._db, self._prop.classname) + if not self.is_edit_ok(): + return self.plain() + + value = self._value 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)) + if sort_on is None: + sort_on = ('+', find_sort_key(linkcl)) + else: + sort_on = ('+', sort_on) + options = linkcl.filter(None, conditions, sort_on) height = height or min(len(options), 7) l = ['' + s = self.input(type="hidden",name="%s",value="%s") if columns and self.columns: l.append(s%(sc+'columns', ','.join(self.columns))) if sort and self.sort[1] is not None: @@ -1660,11 +1931,12 @@ env: %(env)s def base_javascript(self): return ''' -