X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fcgi%2Ftemplating.py;h=3cb9780c909f94bb833018cce663673f09ec2971;hb=e26c0e6a8f9dca8e604bae291509feec1e492354;hp=f536774b4215b0a7e0e04ff540bc2117310c8226;hpb=3eab8d9d3569a12b19c5a859ca21060e9c67ab09;p=roundup.git diff --git a/roundup/cgi/templating.py b/roundup/cgi/templating.py index f536774..3cb9780 100644 --- a/roundup/cgi/templating.py +++ b/roundup/cgi/templating.py @@ -27,6 +27,8 @@ from roundup import hyperdb, date, support from roundup import i18n from roundup.i18n import _ +from KeywordsExpr import render_keywords_expression_editor + try: import cPickle as pickle except ImportError: @@ -115,9 +117,9 @@ def find_template(dir, name, view): if os.path.exists(src): return (src, generic) - raise NoTemplate, 'No template file exists for templating "%s" '\ + raise NoTemplate('No template file exists for templating "%s" ' 'with template "%s" (neither "%s" nor "%s")'%(name, view, - filename, generic) + filename, generic)) class Templates: templates = {} @@ -183,12 +185,28 @@ class Templates: return self.templates[src] # compile the template - self.templates[src] = pt = RoundupPageTemplate() + pt = RoundupPageTemplate() # 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 = stime + # Add it to the cache. We cannot do this until the template + # is fully initialized, as we could otherwise have a race + # condition when running with multiple threads: + # + # 1. Thread A notices the template is not in the cache, + # adds it, but has not yet set "mtime". + # + # 2. Thread B notices the template is in the cache, checks + # "mtime" (above) and crashes. + # + # Since Python dictionary access is atomic, as long as we + # insert "pt" only after it is fully initialized, we avoid + # this race condition. It's possible that two separate + # threads will both do the work of initializing the template, + # but the risk of wasted work is offset by avoiding a lock. + self.templates[src] = pt return pt def __getitem__(self, name): @@ -420,17 +438,19 @@ def _set_input_default_args(dic): except KeyError: pass +def cgi_escape_attrs(**attrs): + return ' '.join(['%s="%s"'%(k,cgi.escape(str(v), True)) + for k,v in attrs.items()]) + def input_html4(**attrs): """Generate an 'input' (html4) element with given attributes""" _set_input_default_args(attrs) - return ''%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True)) - for k,v in attrs.items()]) + return ''%cgi_escape_attrs(**attrs) def input_xhtml(**attrs): """Generate an 'input' (xhtml) element with given attributes""" _set_input_default_args(attrs) - return ''%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True)) - for k,v in attrs.items()]) + return ''%cgi_escape_attrs(**attrs) class HTMLInputMixin: """ requires a _client property """ @@ -502,20 +522,23 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions): def is_edit_ok(self): """ Is the user allowed to Create the current class? """ - return self._db.security.hasPermission('Create', self._client.userid, - self._classname) + perm = self._db.security.hasPermission + return perm('Web Access', self._client.userid) and perm('Create', + self._client.userid, self._classname) def is_retire_ok(self): """ Is the user allowed to retire items of the current class? """ - return self._db.security.hasPermission('Retire', self._client.userid, - self._classname) + perm = self._db.security.hasPermission + return perm('Web Access', self._client.userid) and perm('Retire', + 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) + perm = self._db.security.hasPermission + return perm('Web Access', self._client.userid) and perm('View', + self._client.userid, self._classname) def is_only_view_ok(self): """ Is the user only allowed to View (ie. not Create) the current class? @@ -544,10 +567,7 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions): for klass, htmlklass in propclasses: if not isinstance(prop, klass): continue - if isinstance(prop, hyperdb.Multilink): - value = [] - else: - value = None + value = prop.get_default_value() return htmlklass(self._client, self._classname, None, prop, item, value, self._anonymous) @@ -580,13 +600,10 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions): l = [] 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): + value = prop.get_default_value() l.append(htmlklass(self._client, self._classname, '', - prop, name, value, self._anonymous)) + prop, name, value, self._anonymous)) if sort: l.sort(lambda a,b:cmp(a._name, b._name)) return l @@ -602,6 +619,8 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions): # check perms check = self._client.db.security.hasPermission userid = self._client.userid + if not check('Web Access', userid): + return [] l = [HTMLItem(self._client, self._classname, id) for id in l if check('View', userid, self._classname, itemid=id)] @@ -616,11 +635,14 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions): writer = csv.writer(s) writer.writerow(props) check = self._client.db.security.hasPermission + userid = self._client.userid + if not check('Web Access', userid): + return '' for nodeid in self._klass.list(): l = [] for name in props: # check permission to view this property on this item - if not check('View', self._client.userid, itemid=nodeid, + if not check('View', userid, itemid=nodeid, classname=self._klass.classname, property=name): raise Unauthorised('view', self._klass.classname, translator=self._client.translator) @@ -647,13 +669,23 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions): "request" takes precedence over the other three arguments. """ + security = self._db.security + userid = self._client.userid if request is not None: + # for a request we asume it has already been + # security-filtered filterspec = request.filterspec sort = request.sort group = request.group + else: + cn = self.classname + filterspec = security.filterFilterspec(userid, cn, filterspec) + sort = security.filterSortspec(userid, cn, sort) + group = security.filterSortspec(userid, cn, group) - check = self._db.security.hasPermission - userid = self._client.userid + check = security.hasPermission + if not check('Web Access', userid): + return [] l = [HTMLItem(self._client, self.classname, id) for id in self._klass.filter(None, filterspec, sort, group) @@ -783,20 +815,23 @@ class _HTMLItem(HTMLInputMixin, HTMLPermissions): def is_edit_ok(self): """ Is the user allowed to Edit this item? """ - return self._db.security.hasPermission('Edit', self._client.userid, - self._classname, itemid=self._nodeid) + perm = self._db.security.hasPermission + return perm('Web Access', self._client.userid) and perm('Edit', + self._client.userid, self._classname, itemid=self._nodeid) def is_retire_ok(self): """ Is the user allowed to Reture this item? """ - return self._db.security.hasPermission('Retire', self._client.userid, - self._classname, itemid=self._nodeid) + perm = self._db.security.hasPermission + return perm('Web Access', self._client.userid) and perm('Retire', + self._client.userid, self._classname, itemid=self._nodeid) def is_view_ok(self): """ Is the user allowed to View this item? """ - if self._db.security.hasPermission('View', self._client.userid, - self._classname, itemid=self._nodeid): + perm = self._db.security.hasPermission + if perm('Web Access', self._client.userid) and perm('View', + self._client.userid, self._classname, itemid=self._nodeid): return 1 return self.is_edit_ok() @@ -880,7 +915,8 @@ class _HTMLItem(HTMLInputMixin, HTMLPermissions): # XXX do this return [] - def history(self, direction='descending', dre=re.compile('^\d+$')): + def history(self, direction='descending', dre=re.compile('^\d+$'), + limit=None): if not self.is_view_ok(): return self._('[hidden]') @@ -912,6 +948,10 @@ class _HTMLItem(HTMLInputMixin, HTMLPermissions): history.sort() history.reverse() + # restrict the volume + if limit: + history = history[:limit] + timezone = self._db.getUserTimezone() l = [] comments = {} @@ -1068,6 +1108,13 @@ class _HTMLItem(HTMLInputMixin, HTMLPermissions): cell[-1] += ' -> %s'%current[k] current[k] = val + elif isinstance(prop, hyperdb.Password) and args[k] is not None: + val = args[k].dummystr() + cell.append('%s: %s'%(self._(k), val)) + if current.has_key(k): + cell[-1] += ' -> %s'%current[k] + current[k] = val + elif not args[k]: if current.has_key(k): cell.append('%s: %s'%(self._(k), current[k])) @@ -1182,12 +1229,9 @@ class _HTMLUser(_HTMLItem): return self._db.security.hasPermission(permission, self._nodeid, classname, property, itemid) - def hasRole(self, rolename): - """Determine whether the user has the Role.""" - roles = self._db.user.get(self._nodeid, 'roles').split(',') - for role in roles: - if role.strip() == rolename: return True - return False + def hasRole(self, *rolenames): + """Determine whether the user has any role in rolenames.""" + return self._db.user.has_role(self._nodeid, *rolenames) def HTMLItem(client, classname, nodeid, anonymous=0): if classname == 'user': @@ -1217,7 +1261,12 @@ class HTMLProperty(HTMLInputMixin, HTMLPermissions): self._anonymous = anonymous self._name = name if not anonymous: - self._formname = '%s%s@%s'%(classname, nodeid, name) + if nodeid: + self._formname = '%s%s@%s'%(classname, nodeid, name) + else: + # This case occurs when creating a property for a + # non-anonymous class. + self._formname = '%s@%s'%(classname, name) else: self._formname = name @@ -1243,8 +1292,9 @@ class HTMLProperty(HTMLInputMixin, HTMLPermissions): HTMLInputMixin.__init__(self) def __repr__(self): - return ''%(id(self), self._formname, - self._prop, self._value) + classname = self.__class__.__name__ + return '<%s(0x%x) %s %r %r>'%(classname, id(self), self._formname, + self._prop, self._value) def __str__(self): return self.plain() def __cmp__(self, other): @@ -1264,17 +1314,22 @@ class HTMLProperty(HTMLInputMixin, HTMLPermissions): property. Check "Create" for new items, or "Edit" for existing ones. """ + perm = self._db.security.hasPermission + userid = self._client.userid if self._nodeid: - return self._db.security.hasPermission('Edit', self._client.userid, - self._classname, self._name, self._nodeid) - return self._db.security.hasPermission('Create', self._client.userid, - self._classname, self._name) + if not perm('Web Access', userid): + return False + return perm('Edit', userid, self._classname, self._name, + self._nodeid) + return perm('Create', userid, self._classname, self._name) or \ + perm('Register', userid, self._classname, self._name) def is_view_ok(self): """ Is the user allowed to View the current class? """ - if self._db.security.hasPermission('View', self._client.userid, - self._classname, self._name, self._nodeid): + perm = self._db.security.hasPermission + if perm('Web Access', self._client.userid) and perm('View', + self._client.userid, self._classname, self._name, self._nodeid): return 1 return self.is_edit_ok() @@ -1300,7 +1355,42 @@ class StringHTMLProperty(HTMLProperty): )''', re.X | re.I) protocol_re = re.compile('^(ht|f)tp(s?)://', re.I) - def _hyper_repl_item(self,match,replacement): + + + def _hyper_repl(self, match): + if match.group('url'): + return self._hyper_repl_url(match, '%s%s') + elif match.group('email'): + return self._hyper_repl_email(match, '%s') + elif len(match.group('id')) < 10: + return self._hyper_repl_item(match, + '%(item)s') + else: + # just return the matched text + return match.group(0) + + def _hyper_repl_url(self, match, replacement): + u = s = match.group('url') + if not self.protocol_re.search(s): + u = 'http://' + s + end = '' + if '>' in s: + # catch an escaped ">" in the URL + pos = s.find('>') + end = s[pos:] + u = s = s[:pos] + if ')' in s and s.count('(') != s.count(')'): + # don't include extraneous ')' in the link + pos = s.rfind(')') + end = s[pos:] + end + u = s = s[:pos] + return replacement % (u, s, end) + + def _hyper_repl_email(self, match, replacement): + s = match.group('email') + return replacement % (s, s) + + def _hyper_repl_item(self, match, replacement): item = match.group('item') cls = match.group('class').lower() id = match.group('id') @@ -1313,24 +1403,6 @@ class StringHTMLProperty(HTMLProperty): except KeyError: return item - def _hyper_repl(self, match): - if match.group('url'): - u = s = match.group('url') - if not self.protocol_re.search(s): - u = 'http://' + s - # catch an escaped ">" at the end of the URL - if s.endswith('>'): - u = s = s[:-4] - e = '>' - else: - e = '' - return '%s%s'%(u, s, e) - elif match.group('email'): - s = match.group('email') - return '%s'%(s, s) - else: - return self._hyper_repl_item(match, - '%(item)s') def _hyper_repl_rst(self, match): if match.group('url'): @@ -1339,8 +1411,11 @@ class StringHTMLProperty(HTMLProperty): elif match.group('email'): s = match.group('email') return '`%s `_'%(s, s) - else: + elif len(match.group('id')) < 10: return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_') + else: + # just return the matched text + return match.group(0) def hyperlinked(self): """ Render a "hyperlinked" version of the text """ @@ -1457,8 +1532,7 @@ class StringHTMLProperty(HTMLProperty): value = '"'.join(value.split('"')) name = self._formname - passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True)) - for k,v in kwargs.items()]) + passthrough_args = cgi_escape_attrs(**kwargs) return ('') % locals() @@ -1494,9 +1568,12 @@ class PasswordHTMLProperty(HTMLProperty): if self._value is None: return '' - return self._('*encrypted*') + value = self._value.dummystr() + if escape: + value = cgi.escape(value) + return value - def field(self, size=30): + def field(self, size=30, **kwargs): """ Render a form edit field for the property. If not editable, just display the value via plain(). @@ -1504,7 +1581,8 @@ class PasswordHTMLProperty(HTMLProperty): if not self.is_edit_ok(): return self.plain(escape=1) - return self.input(type="password", name=self._formname, size=size) + return self.input(type="password", name=self._formname, size=size, + **kwargs) def confirm(self, size=30): """ Render a second form edit field for the property, used for @@ -1533,7 +1611,7 @@ class NumberHTMLProperty(HTMLProperty): return str(self._value) - def field(self, size=30): + def field(self, size=30, **kwargs): """ Render a form edit field for the property. If not editable, just display the value via plain(). @@ -1545,7 +1623,8 @@ class NumberHTMLProperty(HTMLProperty): if value is None: value = '' - return self.input(name=self._formname, value=value, size=size) + return self.input(name=self._formname, value=value, size=size, + **kwargs) def __int__(self): """ Return an int of me @@ -1569,7 +1648,7 @@ class BooleanHTMLProperty(HTMLProperty): return '' return self._value and self._("Yes") or self._("No") - def field(self): + def field(self, **kwargs): """ Render a form edit field for the property If not editable, just display the value via plain(). @@ -1585,15 +1664,17 @@ class BooleanHTMLProperty(HTMLProperty): checked = value and "checked" or "" if value: s = self.input(type="radio", name=self._formname, value="yes", - checked="checked") + checked="checked", **kwargs) s += self._('Yes') - s +=self.input(type="radio", name=self._formname, value="no") + s +=self.input(type="radio", name=self._formname, value="no", + **kwargs) s += self._('No') else: - s = self.input(type="radio", name=self._formname, value="yes") + s = self.input(type="radio", name=self._formname, value="yes", + **kwargs) s += self._('Yes') s +=self.input(type="radio", name=self._formname, value="no", - checked="checked") + checked="checked", **kwargs) s += self._('No') return s @@ -1651,7 +1732,8 @@ class DateHTMLProperty(HTMLProperty): return DateHTMLProperty(self._client, self._classname, self._nodeid, self._prop, self._formname, ret) - def field(self, size=30, default=None, format=_marker, popcal=True): + def field(self, size=30, default=None, format=_marker, popcal=True, + **kwargs): """Render a form edit field for the property If not editable, just display the value via plain(). @@ -1686,7 +1768,8 @@ class DateHTMLProperty(HTMLProperty): elif isinstance(value, str) or isinstance(value, unicode): # most likely erroneous input to be passed back to user if isinstance(value, unicode): value = value.encode('utf8') - return self.input(name=self._formname, value=value, size=size) + return self.input(name=self._formname, value=value, size=size, + **kwargs) else: raw_value = value @@ -1706,7 +1789,8 @@ class DateHTMLProperty(HTMLProperty): if format is not self._marker: value = value.pretty(format) - s = self.input(name=self._formname, value=value, size=size) + s = self.input(name=self._formname, value=value, size=size, + **kwargs) if popcal: s += self.popcal() return s @@ -1801,7 +1885,7 @@ class IntervalHTMLProperty(HTMLProperty): return self._value.pretty() - def field(self, size=30): + def field(self, size=30, **kwargs): """ Render a form edit field for the property If not editable, just display the value via plain(). @@ -1813,7 +1897,8 @@ class IntervalHTMLProperty(HTMLProperty): if value is None: value = '' - return self.input(name=self._formname, value=value, size=size) + return self.input(name=self._formname, value=value, size=size, + **kwargs) class LinkHTMLProperty(HTMLProperty): """ Link HTMLProperty @@ -1866,7 +1951,7 @@ class LinkHTMLProperty(HTMLProperty): value = cgi.escape(value) return value - def field(self, showid=0, size=None): + def field(self, showid=0, size=None, **kwargs): """ Render a form edit field for the property If not editable, just display the value via plain(). @@ -1884,10 +1969,11 @@ class LinkHTMLProperty(HTMLProperty): value = linkcl.get(self._value, k) else: value = self._value - return self.input(name=self._formname, value=value, size=size) + return self.input(name=self._formname, value=value, size=size, + **kwargs) def menu(self, size=None, height=None, showid=0, additional=[], value=None, - sort_on=None, **conditions): + sort_on=None, html_kwargs={}, translate=True, **conditions): """ Render a form select list for this property "size" is used to limit the length of the list labels @@ -1900,6 +1986,11 @@ class LinkHTMLProperty(HTMLProperty): (direction, property) where direction is '+' or '-'. A single string with the direction prepended may be used. For example: ('-', 'order'), '+name'. + "html_kwargs" specified additional html args for the + generated html '%self._formname] + l = ['') return '\n'.join(l) @@ -2029,9 +2124,10 @@ class MultilinkHTMLProperty(HTMLProperty): check = self._db.security.hasPermission userid = self._client.userid classname = self._prop.classname - for value in values: - if check('View', userid, classname, itemid=value): - yield HTMLItem(self._client, classname, value) + if check('Web Access', userid): + for value in values: + if check('View', userid, classname, itemid=value): + yield HTMLItem(self._client, classname, value) def __iter__(self): """ iterate and return a new HTMLItem @@ -2086,7 +2182,7 @@ class MultilinkHTMLProperty(HTMLProperty): value = cgi.escape(value) return value - def field(self, size=30, showid=0): + def field(self, size=30, showid=0, **kwargs): """ Render a form edit field for the property If not editable, just display the value via plain(). @@ -2095,18 +2191,23 @@ class MultilinkHTMLProperty(HTMLProperty): return self.plain(escape=1) linkcl = self._db.getclass(self._prop.classname) - value = self._value[:] - # map the id to the label property - if not linkcl.getkey(): - showid=1 - if not showid: - k = linkcl.labelprop(1) - value = lookupKeys(linkcl, k, value) - value = ','.join(value) - return self.input(name=self._formname, size=size, value=value) + + if 'value' not in kwargs: + value = self._value[:] + # map the id to the label property + if not linkcl.getkey(): + showid=1 + if not showid: + k = linkcl.labelprop(1) + value = lookupKeys(linkcl, k, value) + value = ','.join(value) + kwargs["value"] = value + + return self.input(name=self._formname, size=size, **kwargs) def menu(self, size=None, height=None, showid=0, additional=[], - value=None, sort_on=None, **conditions): + value=None, sort_on=None, html_kwargs={}, translate=True, + **conditions): """ Render a form '%(self._formname, height)] + l = ['') return '\n'.join(l) + # set the propclasses for HTMLItem -propclasses = ( +propclasses = [ (hyperdb.String, StringHTMLProperty), (hyperdb.Number, NumberHTMLProperty), (hyperdb.Boolean, BooleanHTMLProperty), @@ -2221,16 +2328,32 @@ propclasses = ( (hyperdb.Password, PasswordHTMLProperty), (hyperdb.Link, LinkHTMLProperty), (hyperdb.Multilink, MultilinkHTMLProperty), -) +] + +def register_propclass(prop, cls): + for index,propclass in enumerate(propclasses): + p, c = propclass + if prop == p: + propclasses[index] = (prop, cls) + break + else: + propclasses.append((prop, cls)) + def make_sort_function(db, classname, sort_on=None): - """Make a sort function for a given class + """Make a sort function for a given class. + + The list being sorted may contain mixed ids and labels. """ linkcl = db.getclass(classname) if sort_on is None: sort_on = linkcl.orderprop() def sortfunc(a, b): - return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on)) + if num_re.match(a): + a = linkcl.get(a, sort_on) + if num_re.match(b): + b = linkcl.get(b, sort_on) + return cmp(a, b) return sortfunc def handleListCGIValue(value): @@ -2350,12 +2473,16 @@ class HTMLRequest(HTMLInputMixin): self.columns = handleListCGIValue(self.form[name]) break self.show = support.TruthDict(self.columns) + security = self._client.db.security + userid = self._client.userid # sorting and grouping self.sort = [] self.group = [] self._parse_sort(self.sort, 'sort') self._parse_sort(self.group, 'group') + self.sort = security.filterSortspec(userid, self.classname, self.sort) + self.group = security.filterSortspec(userid, self.classname, self.group) # filtering self.filter = [] @@ -2385,6 +2512,8 @@ class HTMLRequest(HTMLInputMixin): self.filterspec[name] = handleListCGIValue(fv) else: self.filterspec[name] = fv.value + self.filterspec = security.filterFilterspec(userid, self.classname, + self.filterspec) # full-text search argument self.search_text = None @@ -2601,9 +2730,15 @@ function help_window(helpurl, width, height) { """%self.base - def batch(self): + def batch(self, permission='View'): """ Return a batch object for results from the "current search" """ + check = self._client.db.security.hasPermission + userid = self._client.userid + if not check('Web Access', userid): + return Batch(self.client, [], self.pagesize, self.startwith, + classname=self.classname) + filterspec = self.filterspec sort = self.sort group = self.group @@ -2620,10 +2755,8 @@ function help_window(helpurl, width, height) { matches = None # filter for visibility - check = self._client.db.security.hasPermission - userid = self._client.userid l = [id for id in klass.filter(matches, filterspec, sort, group) - if check('View', userid, self.classname, itemid=id)] + if check(permission, userid, self.classname, itemid=id)] # return the batch object, using IDs only return Batch(self.client, l, self.pagesize, self.startwith, @@ -2748,6 +2881,9 @@ class TemplatingUtils: raise AttributeError, name return self.client.instance.templating_utils[name] + def keywords_expressions(self, request): + return render_keywords_expression_editor(request) + def html_calendar(self, request): """Generate a HTML calendar. @@ -2761,7 +2897,9 @@ class TemplatingUtils: html will simply be a table. """ - date_str = request.form.getfirst("date", ".") + tz = request.client.db.getUserTimezone() + current_date = date.Date(".").local(tz) + date_str = request.form.getfirst("date", current_date) display = request.form.getfirst("display", date_str) template = request.form.getfirst("@template", "calendar") form = request.form.getfirst("form")