X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fcgi%2Ftemplating.py;h=225563e7c30e2311809f3e43a0979bf4f872a2b7;hb=adfcb36bdf8c7a921b1b1760188f481d8f816abb;hp=ae4f0fa3dd31003560c45abd0559c425caa78146;hpb=4a20408dd8ce16b1d159cb7acd78a511ef9b8ad0;p=roundup.git diff --git a/roundup/cgi/templating.py b/roundup/cgi/templating.py index ae4f0fa..225563e 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,54 @@ 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 + ''' + # find the source + if extension: + filename = '%s.%s'%(name, extension) + else: + filename = name + + # try old-style + src = os.path.join(dir, filename) + if os.path.exists(src): + return (src, filename) + + # try with a .html extension (new-style) + filename = filename + '.html' + src = os.path.join(dir, filename) + if os.path.exists(src): + return (src, filename) + + # no extension == no generic template is possible + 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(dir, generic) + if os.path.exists(src): + return (src, generic) + + # finally, try _generic.html + generic = generic + '.html' + src = os.path.join(dir, generic) + if os.path.exists(src): + return (src, generic) + + raise NoTemplate, 'No template file exists for templating "%s" '\ + 'with template "%s" (neither "%s" nor "%s")'%(name, extension, + filename, generic) + class Templates: templates = {} @@ -38,9 +92,9 @@ class Templates: if os.path.isdir(filename): continue if '.' in filename: name, extension = filename.split('.') - self.getTemplate(name, extension) + self.get(name, extension) else: - self.getTemplate(filename, None) + self.get(filename, None) def get(self, name, extension=None): ''' Interface to get a template, possibly loading a compiled template. @@ -59,34 +113,15 @@ class Templates: # split name name, extension = name.split('.') - # find the source, figure the time it was last modified - if extension: - filename = '%s.%s'%(name, extension) - else: - filename = name + # find the source + src, filename = find_template(self.dir, name, extension) - src = os.path.join(self.dir, filename) + # has it changed? 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(src) and \ stime < self.templates[src].mtime: @@ -95,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 @@ -110,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 @@ -166,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): @@ -193,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 ''' @@ -211,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): @@ -222,7 +269,12 @@ class HTMLDatabase: def classes(self): l = self._client.db.classes.keys() l.sort() - return [HTMLClass(self._client, cn) for cn in l] + 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, num_re=re.compile('-?\d+')): cl = db.getclass(prop.classname) @@ -246,17 +298,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): @@ -270,6 +357,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) @@ -320,11 +409,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': @@ -334,7 +427,7 @@ class HTMLClass(HTMLPermissions): return klass(self._client, self.classname, itemid) - def properties(self): + def properties(self, sort=1): ''' Return HTMLProperty for all of this class' properties. ''' l = [] @@ -347,9 +440,11 @@ class HTMLClass(HTMLPermissions): if isinstance(prop, klass): l.append(htmlklass(self._client, self._classname, '', prop, name, value, self._anonymous)) + if sort: + 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': @@ -359,7 +454,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] @@ -368,17 +463,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: @@ -389,7 +480,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): @@ -399,18 +490,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. ''' 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: @@ -419,8 +509,8 @@ class HTMLClass(HTMLPermissions): for x in self._klass.filter(None, filterspec, sort, group)] return l - def classhelp(self, properties=None, label='list', width='500', - height='400'): + def classhelp(self, properties=None, label='(list)', width='500', + height='400', property=''): ''' Pop up a javascript window with class help This generates a link to a popup window which displays the @@ -432,22 +522,32 @@ class HTMLClass(HTMLPermissions): You may optionally override the label displayed, the width and height. The popup window will be resizable and scrollable. + + If the "property" arg is given, it's passed through to the + javascript help_window function. ''' if properties is None: properties = self._klass.getprops(protected=0).keys() properties.sort() properties = ','.join(properties) - return '(%s)'%( - self.classname, properties, width, height, label) + if 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): @@ -462,9 +562,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): @@ -478,6 +582,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) @@ -514,12 +620,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. @@ -528,6 +641,8 @@ class HTMLItem(HTMLPermissions): return [] def history(self, direction='descending', dre=re.compile('\d+')): + self.view_check() + l = ['' '
', _('History'), @@ -552,9 +667,17 @@ class HTMLItem(HTMLPermissions): if (self._props.has_key(prop_n) and isinstance(self._props[prop_n], hyperdb.Link)): classname = self._props[prop_n].classname - if os.path.exists(os.path.join(self._db.config.TEMPLATES, classname + '.item')): - current[prop_n] = '%s'%(classname, - self._klass.get(self._nodeid, prop_n, None), current[prop_n]) + try: + template = find_template(self._db.config.TEMPLATES, + classname, 'item') + if template[1].startswith('_generic'): + raise NoTemplate, 'not really...' + except NoTemplate: + pass + else: + id = self._klass.get(self._nodeid, prop_n, None) + current[prop_n] = '%s'%( + classname, id, current[prop_n]) for id, evt_date, user, action, args in history: date_s = str(evt_date.local(timezone)).replace("."," ") @@ -584,116 +707,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!! @@ -722,7 +852,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') @@ -730,7 +860,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): @@ -751,21 +918,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: @@ -789,6 +942,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) @@ -799,9 +955,29 @@ 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\.\-]+)|' + r'(?P[-+=%/\w\.]+@[\w\.\-]+)|' r'(?P(?P[a-z_]+)(?P\d+)))') def _hyper_repl(self, match): if match.group('url'): @@ -816,18 +992,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: @@ -835,6 +1019,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) @@ -845,37 +1030,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]) @@ -891,38 +1098,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 @@ -939,28 +1172,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())) @@ -971,29 +1219,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) @@ -1007,6 +1268,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: @@ -1015,13 +1278,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) @@ -1029,17 +1296,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 @@ -1073,6 +1350,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] @@ -1084,71 +1363,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() - # 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) 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: @@ -1627,11 +1911,12 @@ env: %(env)s def base_javascript(self): return ''' -