comparison roundup/cgi/templating.py @ 7037:22183e7d1443

flake8 whitepace, indents, var names address: B007 Loop control variable 'id' not used within the loop body. If this is intended, start the name with an underscore. E122 continuation line missing indentation or outdented E127 continuation line over-indented for visual indent E128 continuation line under-indented for visual indent E129 visually indented line with same indent as next logical line E701 multiple statements on one line (colon) E703 statement ends with a semicolon E741 ambiguous variable name 'l'
author John Rouillard <rouilj@ieee.org>
date Mon, 10 Oct 2022 15:43:13 -0400
parents 7847c9bdb631
children ca90f7270cd4 2c89bdc88923
comparison
equal deleted inserted replaced
7036:7847c9bdb631 7037:22183e7d1443
1020 1020
1021 def is_restore_ok(self): 1021 def is_restore_ok(self):
1022 """ Is the user allowed to restore this item? 1022 """ Is the user allowed to restore this item?
1023 """ 1023 """
1024 perm = self._db.security.hasPermission 1024 perm = self._db.security.hasPermission
1025 return perm('Web Access', self._client.userid) and perm('Restore', 1025 return perm('Web Access', self._client.userid) and perm(
1026 self._client.userid, self._classname, itemid=self._nodeid) 1026 'Restore', self._client.userid, self._classname,
1027 itemid=self._nodeid)
1027 1028
1028 def is_view_ok(self): 1029 def is_view_ok(self):
1029 """ Is the user allowed to View this item? 1030 """ Is the user allowed to View this item?
1030 """ 1031 """
1031 perm = self._db.security.hasPermission 1032 perm = self._db.security.hasPermission
1157 1158
1158 timezone = self._db.getUserTimezone() 1159 timezone = self._db.getUserTimezone()
1159 l = [] 1160 l = []
1160 current = {} 1161 current = {}
1161 comments = {} 1162 comments = {}
1162 for id, evt_date, user, action, args in history: 1163 for _id, evt_date, user, action, args in history:
1163 date_s = str(evt_date.local(timezone)).replace(".", " ") 1164 date_s = str(evt_date.local(timezone)).replace(".", " ")
1164 arg_s = '' 1165 arg_s = ''
1165 if action in ['link', 'unlink'] and isinstance(args, tuple): 1166 if action in ['link', 'unlink'] and isinstance(args, tuple):
1166 if len(args) == 3: 1167 if len(args) == 3:
1167 linkcl, linkid, key = args 1168 linkcl, linkid, key = args
1168 arg_s += '<a rel="nofollow noopener" href="%s%s">%s%s %s</a>' % (linkcl, linkid, 1169 arg_s += '<a rel="nofollow noopener" href="%s%s">%s%s %s</a>' % (
1169 linkcl, linkid, key) 1170 linkcl, linkid, linkcl, linkid, key)
1170 else: 1171 else:
1171 arg_s = str(args) 1172 arg_s = str(args)
1172 elif isinstance(args, dict): 1173 elif isinstance(args, dict):
1173 cell = [] 1174 cell = []
1174 for k in args.keys(): 1175 for k in args.keys():
1181 if prop is None: 1182 if prop is None:
1182 # property no longer exists 1183 # property no longer exists
1183 comments['no_exist'] = self._( 1184 comments['no_exist'] = self._(
1184 "<em>The indicated property no longer exists</em>") 1185 "<em>The indicated property no longer exists</em>")
1185 cell.append(self._('<em>%s: %s</em>\n') 1186 cell.append(self._('<em>%s: %s</em>\n')
1186 % (self._(k), str(args[k]))) 1187 % (self._(k), str(args[k])))
1187 continue 1188 continue
1188 1189
1189 # load the current state for the property (if we 1190 # load the current state for the property (if we
1190 # haven't already) 1191 # haven't already)
1191 if k not in current: 1192 if k not in current:
1207 linkid = self._klass.get(self._nodeid, k, None) 1208 linkid = self._klass.get(self._nodeid, k, None)
1208 current[k] = '<a rel="nofollow noopener" href="%s%s">%s</a>' % ( 1209 current[k] = '<a rel="nofollow noopener" href="%s%s">%s</a>' % (
1209 classname, linkid, current[k]) 1210 classname, linkid, current[k])
1210 1211
1211 if args[k] and (isinstance(prop, hyperdb.Multilink) or 1212 if args[k] and (isinstance(prop, hyperdb.Multilink) or
1212 isinstance(prop, hyperdb.Link)): 1213 isinstance(prop, hyperdb.Link)):
1213 # figure what the link class is 1214 # figure what the link class is
1214 classname = prop.classname 1215 classname = prop.classname
1215 try: 1216 try:
1216 linkcl = self._db.getclass(classname) 1217 linkcl = self._db.getclass(classname)
1217 except KeyError: 1218 except KeyError:
1220 "The linked class %(classname)s no longer exists" 1221 "The linked class %(classname)s no longer exists"
1221 ) % locals() 1222 ) % locals()
1222 labelprop = linkcl.labelprop(1) 1223 labelprop = linkcl.labelprop(1)
1223 try: 1224 try:
1224 template = self._client.selectTemplate(classname, 1225 template = self._client.selectTemplate(classname,
1225 'item') 1226 'item')
1226 if template.startswith('_generic.'): 1227 if template.startswith('_generic.'):
1227 raise NoTemplate('not really...') 1228 raise NoTemplate('not really...')
1228 hrefable = 1 1229 hrefable = 1
1229 except NoTemplate: 1230 except NoTemplate:
1230 hrefable = 0 1231 hrefable = 0
1249 # TODO: test for node existence even when 1250 # TODO: test for node existence even when
1250 # there's no labelprop! 1251 # there's no labelprop!
1251 try: 1252 try:
1252 if labelprop is not None and \ 1253 if labelprop is not None and \
1253 labelprop != 'id': 1254 labelprop != 'id':
1254 label = linkcl.get(linkid, labelprop, 1255 label = linkcl.get(
1255 default=self._( 1256 linkid, labelprop,
1256 "[label is missing]")) 1257 default=self._(
1258 "[label is missing]"))
1257 label = html_escape(label) 1259 label = html_escape(label)
1258 except IndexError: 1260 except IndexError:
1259 comments['no_link'] = self._( 1261 comments['no_link'] = self._(
1260 "<strike>The linked node" 1262 "<strike>The linked node"
1261 " no longer exists</strike>") 1263 " no longer exists</strike>")
1262 subml.append('<strike>%s</strike>' % label) 1264 subml.append('<strike>%s</strike>' % label)
1263 else: 1265 else:
1264 if hrefable: 1266 if hrefable:
1265 subml.append('<a rel="nofollow noopener" ' 1267 subml.append(
1266 'href="%s%s">%s</a>' % ( 1268 '<a rel="nofollow noopener" '
1267 classname, linkid, label)) 1269 'href="%s%s">%s</a>' % (
1270 classname, linkid, label))
1268 elif label is None: 1271 elif label is None:
1269 subml.append('%s%s' % (classname, 1272 subml.append('%s%s' % (classname,
1270 linkid)) 1273 linkid))
1271 else: 1274 else:
1272 subml.append(label) 1275 subml.append(label)
1273 ml.append(sublabel + ', '.join(subml)) 1276 ml.append(sublabel + ', '.join(subml))
1274 cell.append('%s:\n %s' % (self._(k), ', '.join(ml))) 1277 cell.append('%s:\n %s' % (self._(k), ', '.join(ml)))
1275 elif isinstance(prop, hyperdb.Link) and args[k]: 1278 elif isinstance(prop, hyperdb.Link) and args[k]:
1277 # if we have a label property, try to use it 1280 # if we have a label property, try to use it
1278 # TODO: test for node existence even when 1281 # TODO: test for node existence even when
1279 # there's no labelprop! 1282 # there's no labelprop!
1280 if labelprop is not None and labelprop != 'id': 1283 if labelprop is not None and labelprop != 'id':
1281 try: 1284 try:
1282 label = html_escape(linkcl.get(args[k], 1285 label = html_escape(
1283 labelprop, default=self._( 1286 linkcl.get(args[k],
1284 "[label is missing]"))) 1287 labelprop, default=self._(
1288 "[label is missing]")))
1285 except IndexError: 1289 except IndexError:
1286 comments['no_link'] = self._( 1290 comments['no_link'] = self._(
1287 "<strike>The linked node" 1291 "<strike>The linked node"
1288 " no longer exists</strike>") 1292 " no longer exists</strike>")
1289 cell.append(' <strike>%s</strike>,\n' % label) 1293 cell.append(' <strike>%s</strike>,\n' % label)
1290 # "flag" this is done .... euwww 1294 # "flag" this is done .... euwww
1291 label = None 1295 label = None
1292 if label is not None: 1296 if label is not None:
1293 if hrefable: 1297 if hrefable:
1294 old = '<a rel="nofollow noopener" href="%s%s">%s</a>' % (classname, 1298 old = '<a rel="nofollow noopener" href="%s%s">%s</a>' % (
1295 args[k], label) 1299 classname, args[k], label)
1296 else: 1300 else:
1297 old = label; 1301 old = label
1298 cell.append('%s: %s' % (self._(k), old)) 1302 cell.append('%s: %s' % (self._(k), old))
1299 if k in current and current[k] is not None: 1303 if k in current and current[k] is not None:
1300 cell[-1] += ' -> %s' % current[k] 1304 cell[-1] += ' -> %s' % current[k]
1301 current[k] = old 1305 current[k] = old
1302 1306
1303 elif isinstance(prop, hyperdb.Date) and args[k]: 1307 elif isinstance(prop, hyperdb.Date) and args[k]:
1304 if args[k] is None: 1308 if args[k] is None:
1305 d = '' 1309 d = ''
1306 else: 1310 else:
1307 d = date.Date(args[k], 1311 d = date.Date(
1312 args[k],
1308 translator=self._client).local(timezone) 1313 translator=self._client).local(timezone)
1309 cell.append('%s: %s' % (self._(k), str(d))) 1314 cell.append('%s: %s' % (self._(k), str(d)))
1310 if k in current and current[k] is not None: 1315 if k in current and current[k] is not None:
1311 cell[-1] += ' -> %s' % current[k] 1316 cell[-1] += ' -> %s' % current[k]
1312 current[k] = str(d) 1317 current[k] = str(d)
1313 1318
1314 elif isinstance(prop, hyperdb.Interval) and args[k]: 1319 elif isinstance(prop, hyperdb.Interval) and args[k]:
1315 val = str(date.Interval(args[k], 1320 val = str(date.Interval(args[k],
1316 translator=self._client)) 1321 translator=self._client))
1317 cell.append('%s: %s' % (self._(k), val)) 1322 cell.append('%s: %s' % (self._(k), val))
1318 if k in current and current[k] is not None: 1323 if k in current and current[k] is not None:
1319 cell[-1] += ' -> %s' % current[k] 1324 cell[-1] += ' -> %s' % current[k]
1320 current[k] = val 1325 current[k] = val
1321 1326
1380 1385
1381 if direction == 'ascending': 1386 if direction == 'ascending':
1382 l.reverse() 1387 l.reverse()
1383 1388
1384 l[0:0] = ['<table class="history table table-condensed table-striped">' 1389 l[0:0] = ['<table class="history table table-condensed table-striped">'
1385 '<tr><th colspan="4" class="header">', 1390 '<tr><th colspan="4" class="header">',
1386 self._('History'), 1391 self._('History'),
1387 '</th></tr><tr>', 1392 '</th></tr><tr>',
1388 self._('<th>Date</th>'), 1393 self._('<th>Date</th>'),
1389 self._('<th>User</th>'), 1394 self._('<th>User</th>'),
1390 self._('<th>Action</th>'), 1395 self._('<th>Action</th>'),
1391 self._('<th>Args</th>'), 1396 self._('<th>Args</th>'),
1392 '</tr>'] 1397 '</tr>']
1393 l.append('</table>') 1398 l.append('</table>')
1394 1399
1395 self._client.form_wins = orig_form_wins 1400 self._client.form_wins = orig_form_wins
1396 1401
1397 return '\n'.join(l) 1402 return '\n'.join(l)
1402 # create a new request and override the specified args 1407 # create a new request and override the specified args
1403 req = HTMLRequest(self._client) 1408 req = HTMLRequest(self._client)
1404 req.classname = self._klass.get(self._nodeid, 'klass') 1409 req.classname = self._klass.get(self._nodeid, 'klass')
1405 name = self._klass.get(self._nodeid, 'name') 1410 name = self._klass.get(self._nodeid, 'name')
1406 req.updateFromURL(self._klass.get(self._nodeid, 'url') + 1411 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1407 '&@queryname=%s' % urllib_.quote(name)) 1412 '&@queryname=%s' % urllib_.quote(name))
1408 1413
1409 # new template, using the specified classname and request 1414 # new template, using the specified classname and request
1410 # [ ] the custom logic for search page doesn't belong to 1415 # [ ] the custom logic for search page doesn't belong to
1411 # generic templating module (techtonik) 1416 # generic templating module (techtonik)
1412 tplname = self._client.selectTemplate(req.classname, 'search') 1417 tplname = self._client.selectTemplate(req.classname, 'search')
1459 """Add ability to check for permissions on users. 1464 """Add ability to check for permissions on users.
1460 """ 1465 """
1461 _marker = [] 1466 _marker = []
1462 1467
1463 def hasPermission(self, permission, classname=_marker, 1468 def hasPermission(self, permission, classname=_marker,
1464 property=None, itemid=None): 1469 property=None, itemid=None):
1465 """Determine if the user has the Permission. 1470 """Determine if the user has the Permission.
1466 1471
1467 The class being tested defaults to the template's class, but may 1472 The class being tested defaults to the template's class, but may
1468 be overidden for this test by suppling an alternate classname. 1473 be overidden for this test by suppling an alternate classname.
1469 """ 1474 """
1470 if classname is self._marker: 1475 if classname is self._marker:
1471 classname = self._client.classname 1476 classname = self._client.classname
1472 return self._db.security.hasPermission(permission, 1477 return self._db.security.hasPermission(
1473 self._nodeid, classname, property, itemid) 1478 permission, self._nodeid, classname, property, itemid)
1474 1479
1475 def hasRole(self, *rolenames): 1480 def hasRole(self, *rolenames):
1476 """Determine whether the user has any role in rolenames.""" 1481 """Determine whether the user has any role in rolenames."""
1477 return self._db.user.has_role(self._nodeid, *rolenames) 1482 return self._db.user.has_role(self._nodeid, *rolenames)
1478 1483
1493 _value the value of the property if any 1498 _value the value of the property if any
1494 1499
1495 A wrapper object which may be stringified for the plain() behaviour. 1500 A wrapper object which may be stringified for the plain() behaviour.
1496 """ 1501 """
1497 def __init__(self, client, classname, nodeid, prop, name, value, 1502 def __init__(self, client, classname, nodeid, prop, name, value,
1498 anonymous=0): 1503 anonymous=0):
1499 self._client = client 1504 self._client = client
1500 self._db = client.db 1505 self._db = client.db
1501 self._ = client._ 1506 self._ = client._
1502 self._classname = classname 1507 self._classname = classname
1503 self._nodeid = nodeid 1508 self._nodeid = nodeid
1600 userid = self._client.userid 1605 userid = self._client.userid
1601 if self._nodeid: 1606 if self._nodeid:
1602 if not perm('Web Access', userid): 1607 if not perm('Web Access', userid):
1603 return False 1608 return False
1604 return perm('Edit', userid, self._classname, self._name, 1609 return perm('Edit', userid, self._classname, self._name,
1605 self._nodeid) 1610 self._nodeid)
1606 return perm('Create', userid, self._classname, self._name) or \ 1611 return perm('Create', userid, self._classname, self._name) or \
1607 perm('Register', userid, self._classname, self._name) 1612 perm('Register', userid, self._classname, self._name)
1608 1613
1609 def is_view_ok(self): 1614 def is_view_ok(self):
1610 """ Is the user allowed to View the current class? 1615 """ Is the user allowed to View the current class?
1611 """ 1616 """
1612 perm = self._db.security.hasPermission 1617 perm = self._db.security.hasPermission
1613 if perm('Web Access', self._client.userid) and perm('View', 1618 if perm('Web Access', self._client.userid) and perm(
1614 self._client.userid, self._classname, self._name, self._nodeid): 1619 'View', self._client.userid, self._classname,
1620 self._name, self._nodeid):
1615 return 1 1621 return 1
1616 return self.is_edit_ok() 1622 return self.is_edit_ok()
1617 1623
1618 1624
1619 class StringHTMLProperty(HTMLProperty): 1625 class StringHTMLProperty(HTMLProperty):
1649 if match.group('url'): 1655 if match.group('url'):
1650 return self._hyper_repl_url(match, '<a href="%s" rel="nofollow noopener">%s</a>%s') 1656 return self._hyper_repl_url(match, '<a href="%s" rel="nofollow noopener">%s</a>%s')
1651 elif match.group('email'): 1657 elif match.group('email'):
1652 return self._hyper_repl_email(match, '<a href="mailto:%s">%s</a>') 1658 return self._hyper_repl_email(match, '<a href="mailto:%s">%s</a>')
1653 elif len(match.group('id')) < 10: 1659 elif len(match.group('id')) < 10:
1654 return self._hyper_repl_item(match, 1660 return self._hyper_repl_item(
1655 '<a href="%(cls)s%(id)s%(fragment)s">%(item)s</a>') 1661 match, '<a href="%(cls)s%(id)s%(fragment)s">%(item)s</a>')
1656 else: 1662 else:
1657 # just return the matched text 1663 # just return the matched text
1658 return match.group(0) 1664 return match.group(0)
1659 1665
1660 def _hyper_repl_url(self, match, replacement): 1666 def _hyper_repl_url(self, match, replacement):
1856 if sch in self.valid_schemes: 1862 if sch in self.valid_schemes:
1857 pass 1863 pass
1858 else: 1864 else:
1859 raise 1865 raise
1860 1866
1861 return u2s(ReStructuredText(s, writer_name="html", 1867 return u2s(ReStructuredText(
1862 settings_overrides=self.rst_defaults)["html_body"]) 1868 s, writer_name="html",
1869 settings_overrides=self.rst_defaults)["html_body"])
1863 1870
1864 def markdown(self, hyperlink=1): 1871 def markdown(self, hyperlink=1):
1865 """ Render the value of the property as markdown. 1872 """ Render the value of the property as markdown.
1866 1873
1867 This requires markdown2 or markdown to be installed separately. 1874 This requires markdown2 or markdown to be installed separately.
1912 value = '&quot;'.join(value.split('"')) 1919 value = '&quot;'.join(value.split('"'))
1913 name = self._formname 1920 name = self._formname
1914 passthrough_args = self.cgi_escape_attrs(**kwargs) 1921 passthrough_args = self.cgi_escape_attrs(**kwargs)
1915 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"' 1922 return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1916 ' rows="%(rows)s" cols="%(cols)s">' 1923 ' rows="%(rows)s" cols="%(cols)s">'
1917 '%(value)s</textarea>') % locals() 1924 '%(value)s</textarea>') % locals()
1918 1925
1919 def email(self, escape=1): 1926 def email(self, escape=1):
1920 """ Render the value of the property as an obscured email address 1927 """ Render the value of the property as an obscured email address
1921 """ 1928 """
1922 if not self.is_view_ok(): 1929 if not self.is_view_ok():
2100 return self.plain(escape=1) 2107 return self.plain(escape=1)
2101 2108
2102 value = self._value 2109 value = self._value
2103 if is_us(value): 2110 if is_us(value):
2104 value = value.strip().lower() in ('checked', 'yes', 'true', 2111 value = value.strip().lower() in ('checked', 'yes', 'true',
2105 'on', '1') 2112 'on', '1')
2106 2113
2107 if (not y_label): 2114 if (not y_label):
2108 y_label = '<label class="rblabel" for="%s_%s">' % ( 2115 y_label = '<label class="rblabel" for="%s_%s">' % (
2109 self._formname, 'yes') 2116 self._formname, 'yes')
2110 y_label += self._('Yes') 2117 y_label += self._('Yes')
2117 n_label += '</label>' 2124 n_label += '</label>'
2118 2125
2119 checked = value and "checked" or "" 2126 checked = value and "checked" or ""
2120 if value: 2127 if value:
2121 y_rb = self.input(type="radio", name=self._formname, value="yes", 2128 y_rb = self.input(type="radio", name=self._formname, value="yes",
2122 checked="checked", id="%s_%s" % ( 2129 checked="checked", id="%s_%s" % (
2123 self._formname, 'yes'), **kwargs) 2130 self._formname, 'yes'), **kwargs)
2124 2131
2125 n_rb = self.input(type="radio", name=self._formname, value="no", 2132 n_rb = self.input(type="radio", name=self._formname, value="no",
2126 id="%s_%s" % ( 2133 id="%s_%s" % (
2127 self._formname, 'no'), **kwargs) 2134 self._formname, 'no'), **kwargs)
2128 else: 2135 else:
2129 y_rb = self.input(type="radio", name=self._formname, value="yes", 2136 y_rb = self.input(type="radio", name=self._formname, value="yes",
2130 id="%s_%s" % (self._formname, 'yes'), **kwargs) 2137 id="%s_%s" % (self._formname, 'yes'), **kwargs)
2131 2138
2132 n_rb = self.input(type="radio", name=self._formname, value="no", 2139 n_rb = self.input(type="radio", name=self._formname, value="no",
2133 checked="checked", id="%s_%s" % ( 2140 checked="checked", id="%s_%s" % (
2134 self._formname, 'no'), **kwargs) 2141 self._formname, 'no'), **kwargs)
2135 2142
2136 if (u_label): 2143 if (u_label):
2137 if (u_label is True): # it was set via u_label=True 2144 if (u_label is True): # it was set via u_label=True
2138 u_label = '' # make it empty but a string not boolean 2145 u_label = '' # make it empty but a string not boolean
2139 u_rb = self.input(type="radio", name=self._formname, value="", 2146 u_rb = self.input(type="radio", name=self._formname, value="",
2140 id="%s_%s" % (self._formname, 'unk'), **kwargs) 2147 id="%s_%s" % (self._formname, 'unk'), **kwargs)
2141 else: 2148 else:
2142 # don't generate a trivalue radiobutton. 2149 # don't generate a trivalue radiobutton.
2143 u_label = '' 2150 u_label = ''
2144 u_rb = '' 2151 u_rb = ''
2145 2152
2154 class DateHTMLProperty(HTMLProperty): 2161 class DateHTMLProperty(HTMLProperty):
2155 2162
2156 _marker = [] 2163 _marker = []
2157 2164
2158 def __init__(self, client, classname, nodeid, prop, name, value, 2165 def __init__(self, client, classname, nodeid, prop, name, value,
2159 anonymous=0, offset=None): 2166 anonymous=0, offset=None):
2160 HTMLProperty.__init__(self, client, classname, nodeid, prop, name, 2167 HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
2161 value, anonymous=anonymous) 2168 value, anonymous=anonymous)
2162 if self._value and not is_us(self._value): 2169 if self._value and not is_us(self._value):
2163 self._value.setTranslator(self._client.translator) 2170 self._value.setTranslator(self._client.translator)
2164 self._offset = offset 2171 self._offset = offset
2165 if self._offset is None: 2172 if self._offset is None:
2166 self._offset = self._prop.offset(self._db) 2173 self._offset = self._prop.offset(self._db)
2204 ret = ret + interval 2211 ret = ret + interval
2205 else: 2212 else:
2206 ret = ret - interval 2213 ret = ret - interval
2207 2214
2208 return DateHTMLProperty(self._client, self._classname, self._nodeid, 2215 return DateHTMLProperty(self._client, self._classname, self._nodeid,
2209 self._prop, self._formname, ret) 2216 self._prop, self._formname, ret)
2210 2217
2211 def field(self, size=30, default=None, format=_marker, popcal=True, 2218 def field(self, size=30, default=None, format=_marker, popcal=True,
2212 **kwargs): 2219 **kwargs):
2213 """Render a form edit field for the property 2220 """Render a form edit field for the property
2214 2221
2236 elif isinstance(default, date.Date): 2243 elif isinstance(default, date.Date):
2237 raw_value = default 2244 raw_value = default
2238 elif isinstance(default, DateHTMLProperty): 2245 elif isinstance(default, DateHTMLProperty):
2239 raw_value = default._value 2246 raw_value = default._value
2240 else: 2247 else:
2241 raise ValueError(self._('default value for ' 2248 raise ValueError(self._(
2249 'default value for '
2242 'DateHTMLProperty must be either DateHTMLProperty ' 2250 'DateHTMLProperty must be either DateHTMLProperty '
2243 'or string date representation.')) 2251 'or string date representation.'))
2244 elif is_us(value): 2252 elif is_us(value):
2245 # most likely erroneous input to be passed back to user 2253 # most likely erroneous input to be passed back to user
2246 value = us2s(value) 2254 value = us2s(value)
2247 s = self.input(name=self._formname, value=value, size=size, 2255 s = self.input(name=self._formname, value=value, size=size,
2248 **kwargs) 2256 **kwargs)
2249 if popcal: 2257 if popcal:
2250 s += self.popcal() 2258 s += self.popcal()
2251 return s 2259 return s
2252 else: 2260 else:
2253 raw_value = value 2261 raw_value = value
2323 """ 2331 """
2324 if not self.is_view_ok(): 2332 if not self.is_view_ok():
2325 return self._('[hidden]') 2333 return self._('[hidden]')
2326 2334
2327 return DateHTMLProperty(self._client, self._classname, self._nodeid, 2335 return DateHTMLProperty(self._client, self._classname, self._nodeid,
2328 self._prop, self._formname, self._value, offset=offset) 2336 self._prop, self._formname, self._value,
2337 offset=offset)
2329 2338
2330 def popcal(self, width=300, height=200, label="(cal)", 2339 def popcal(self, width=300, height=200, label="(cal)",
2331 form="itemSynopsis"): 2340 form="itemSynopsis"):
2332 """Generate a link to a calendar pop-up window. 2341 """Generate a link to a calendar pop-up window.
2333 2342
2334 item: HTMLProperty e.g.: context.deadline 2343 item: HTMLProperty e.g.: context.deadline
2335 """ 2344 """
2336 if self.isset(): 2345 if self.isset():
2344 "data-width": width, 2353 "data-width": width,
2345 "data-height": height 2354 "data-height": height
2346 } 2355 }
2347 2356
2348 return ('<a class="classhelp" %s href="javascript:help_window(' 2357 return ('<a class="classhelp" %s href="javascript:help_window('
2349 "'%s?@template=calendar&amp;property=%s&amp;form=%s%s', %d, %d)" 2358 "'%s?@template=calendar&amp;property=%s&amp;form=%s%s', %d, %d)"
2350 '">%s</a>' % (self.cgi_escape_attrs(**data_attr), 2359 '">%s</a>' % (self.cgi_escape_attrs(**data_attr),
2351 self._classname, self._name, form, date, width, 2360 self._classname, self._name, form, date, width,
2352 height, label)) 2361 height, label))
2353 2362
2354 2363
2355 class IntervalHTMLProperty(HTMLProperty): 2364 class IntervalHTMLProperty(HTMLProperty):
2356 def __init__(self, client, classname, nodeid, prop, name, value, 2365 def __init__(self, client, classname, nodeid, prop, name, value,
2357 anonymous=0): 2366 anonymous=0):
2358 HTMLProperty.__init__(self, client, classname, nodeid, prop, 2367 HTMLProperty.__init__(self, client, classname, nodeid, prop,
2359 name, value, anonymous) 2368 name, value, anonymous)
2360 if self._value and not is_us(self._value): 2369 if self._value and not is_us(self._value):
2361 self._value.setTranslator(self._client.translator) 2370 self._value.setTranslator(self._client.translator)
2362 2371
2363 def plain(self, escape=0): 2372 def plain(self, escape=0):
2364 """ Render a "plain" representation of the property 2373 """ Render a "plain" representation of the property
2526 value = self._value 2535 value = self._value
2527 elif value == '-1': 2536 elif value == '-1':
2528 value = None 2537 value = None
2529 2538
2530 linkcl = self._db.getclass(self._prop.classname) 2539 linkcl = self._db.getclass(self._prop.classname)
2531 l = ['<select %s>' % self.cgi_escape_attrs(name=self._formname, 2540 html = ['<select %s>' % self.cgi_escape_attrs(name=self._formname,
2532 **html_kwargs)] 2541 **html_kwargs)]
2533 k = linkcl.labelprop(1) 2542 k = linkcl.labelprop(1)
2534 s = '' 2543 s = ''
2535 if value is None: 2544 if value is None:
2536 s = 'selected="selected" ' 2545 s = 'selected="selected" '
2537 l.append(self._('<option %svalue="-1">- no selection -</option>') % s) 2546 html.append(self._(
2547 '<option %svalue="-1">- no selection -</option>') % s)
2538 2548
2539 if sort_on is not None: 2549 if sort_on is not None:
2540 if not isinstance(sort_on, tuple): 2550 if not isinstance(sort_on, tuple):
2541 if sort_on[0] in '+-': 2551 if sort_on[0] in '+-':
2542 sort_on = (sort_on[0], sort_on[1:]) 2552 sort_on = (sort_on[0], sort_on[1:])
2544 sort_on = ('+', sort_on) 2554 sort_on = ('+', sort_on)
2545 else: 2555 else:
2546 sort_on = ('+', linkcl.orderprop()) 2556 sort_on = ('+', linkcl.orderprop())
2547 2557
2548 options = [opt 2558 options = [opt
2549 for opt in linkcl.filter(None, conditions, sort_on, (None, None)) 2559 for opt in linkcl.filter(
2550 if self._db.security.hasPermission("View", self._client.userid, 2560 None, conditions, sort_on, (None, None))
2551 linkcl.classname, itemid=opt)] 2561 if self._db.security.hasPermission(
2562 "View", self._client.userid, linkcl.classname,
2563 itemid=opt)]
2552 2564
2553 # make sure we list the current value if it's retired 2565 # make sure we list the current value if it's retired
2554 if value and value not in options: 2566 if value and value not in options:
2555 options.insert(0, value) 2567 options.insert(0, value)
2556 2568
2612 # and generate 2624 # and generate
2613 tr = str 2625 tr = str
2614 if translate: 2626 if translate:
2615 tr = self._ 2627 tr = self._
2616 lab = html_escape(tr(lab)) 2628 lab = html_escape(tr(lab))
2617 l.append('<option %svalue="%s">%s</option>' % (s, optionid, lab)) 2629 html.append(
2618 l.append('</select>') 2630 '<option %svalue="%s">%s</option>' % (s, optionid, lab))
2619 return '\n'.join(l) 2631 html.append('</select>')
2632 return '\n'.join(html)
2620 2633
2621 # def checklist(self, ...) 2634 # def checklist(self, ...)
2622 2635
2623 2636
2624 class MultilinkHTMLProperty(HTMLProperty): 2637 class MultilinkHTMLProperty(HTMLProperty):
2629 """ 2642 """
2630 def __init__(self, *args, **kwargs): 2643 def __init__(self, *args, **kwargs):
2631 HTMLProperty.__init__(self, *args, **kwargs) 2644 HTMLProperty.__init__(self, *args, **kwargs)
2632 if self._value: 2645 if self._value:
2633 display_value = lookupIds(self._db, self._prop, self._value, 2646 display_value = lookupIds(self._db, self._prop, self._value,
2634 fail_ok=1, do_lookup=False) 2647 fail_ok=1, do_lookup=False)
2635 keyfun = make_key_function(self._db, self._prop.classname) 2648 keyfun = make_key_function(self._db, self._prop.classname)
2636 # sorting fails if the value contains 2649 # sorting fails if the value contains
2637 # items not yet stored in the database 2650 # items not yet stored in the database
2638 # ignore these errors to preserve user input 2651 # ignore these errors to preserve user input
2639 try: 2652 try:
2668 return self.viewableGenerator(self._value) 2681 return self.viewableGenerator(self._value)
2669 2682
2670 def reverse(self): 2683 def reverse(self):
2671 """ return the list in reverse order 2684 """ return the list in reverse order
2672 """ 2685 """
2673 l = self._value[:] 2686 mylist = self._value[:]
2674 l.reverse() 2687 mylist.reverse()
2675 return self.viewableGenerator(l) 2688 return self.viewableGenerator(mylist)
2676 2689
2677 def sorted(self, property, reverse=False, NoneFirst=False): 2690 def sorted(self, property, reverse=False, NoneFirst=False):
2678 """ Return this multilink sorted by the given property 2691 """ Return this multilink sorted by the given property
2679 2692
2680 Set Nonefirst to True to sort None/unset property 2693 Set Nonefirst to True to sort None/unset property
2749 label = linkcl.get(v, k, 2762 label = linkcl.get(v, k,
2750 default=self._("[label is missing]")) 2763 default=self._("[label is missing]"))
2751 except IndexError: 2764 except IndexError:
2752 label = None 2765 label = None
2753 # fall back to designator if label is None 2766 # fall back to designator if label is None
2754 if label is None: label = '%s%s' % (self._prop.classname, k) 2767 if label is None:
2768 label = '%s%s' % (self._prop.classname, k)
2755 else: 2769 else:
2756 label = v 2770 label = v
2757 labels.append(label) 2771 labels.append(label)
2758 value = ', '.join(labels) 2772 value = ', '.join(labels)
2759 if escape: 2773 if escape:
2825 sort_on = ('+', sort_on) 2839 sort_on = ('+', sort_on)
2826 else: 2840 else:
2827 sort_on = ('+', linkcl.orderprop()) 2841 sort_on = ('+', linkcl.orderprop())
2828 2842
2829 options = [opt 2843 options = [opt
2830 for opt in linkcl.filter(None, conditions, sort_on) 2844 for opt in linkcl.filter(None, conditions, sort_on)
2831 if self._db.security.hasPermission("View", self._client.userid, 2845 if self._db.security.hasPermission(
2832 linkcl.classname, itemid=opt)] 2846 "View", self._client.userid, linkcl.classname,
2847 itemid=opt)]
2833 2848
2834 # make sure we list the current values if they're retired 2849 # make sure we list the current values if they're retired
2835 for val in value: 2850 for val in value:
2836 if val not in options: 2851 if val not in options:
2837 options.insert(0, val) 2852 options.insert(0, val)
2840 height = len(options) 2855 height = len(options)
2841 if value: 2856 if value:
2842 # The "no selection" option. 2857 # The "no selection" option.
2843 height += 1 2858 height += 1
2844 height = min(height, 7) 2859 height = min(height, 7)
2845 l = ['<select multiple %s>' % self.cgi_escape_attrs( 2860 html = ['<select multiple %s>' % self.cgi_escape_attrs(
2846 name=self._formname, size=height, **html_kwargs)] 2861 name=self._formname, size=height, **html_kwargs)]
2847 k = linkcl.labelprop(1) 2862 k = linkcl.labelprop(1)
2848 2863
2849 if value: # FIXME '- no selection -' mark for translation 2864 if value: # FIXME '- no selection -' mark for translation
2850 l.append('<option value="%s">- no selection -</option>' 2865 html.append('<option value="%s">- no selection -</option>'
2851 % ','.join(['-' + v for v in value])) 2866 % ','.join(['-' + v for v in value]))
2852 2867
2853 if additional: 2868 if additional:
2854 additional_fns = [] 2869 additional_fns = []
2855 props = linkcl.getprops() 2870 props = linkcl.getprops()
2856 for propname in additional: 2871 for propname in additional:
2892 # and generate 2907 # and generate
2893 tr = str 2908 tr = str
2894 if translate: 2909 if translate:
2895 tr = self._ 2910 tr = self._
2896 lab = html_escape(tr(lab)) 2911 lab = html_escape(tr(lab))
2897 l.append('<option %svalue="%s">%s</option>' % (s, optionid, 2912 html.append('<option %svalue="%s">%s</option>' % (s, optionid,
2898 lab)) 2913 lab))
2899 l.append('</select>') 2914 html.append('</select>')
2900 return '\n'.join(l) 2915 return '\n'.join(html)
2901 2916
2902 2917
2903 # set the propclasses for HTMLItem 2918 # set the propclasses for HTMLItem
2904 propclasses = [ 2919 propclasses = [
2905 (hyperdb.String, StringHTMLProperty), 2920 (hyperdb.String, StringHTMLProperty),
3109 prop = cls.get_transitive_prop(name) 3124 prop = cls.get_transitive_prop(name)
3110 fv = self.form[name] 3125 fv = self.form[name]
3111 if (isinstance(prop, hyperdb.Link) or 3126 if (isinstance(prop, hyperdb.Link) or
3112 isinstance(prop, hyperdb.Multilink)): 3127 isinstance(prop, hyperdb.Multilink)):
3113 self.filterspec[name] = lookupIds(db, prop, 3128 self.filterspec[name] = lookupIds(db, prop,
3114 handleListCGIValue(fv)) 3129 handleListCGIValue(fv))
3115 else: 3130 else:
3116 if isinstance(fv, type([])): 3131 if isinstance(fv, type([])):
3117 self.filterspec[name] = [v.value for v in fv] 3132 self.filterspec[name] = [v.value for v in fv]
3118 elif name == 'id': 3133 elif name == 'id':
3119 # special case "id" property 3134 # special case "id" property
3120 self.filterspec[name] = handleListCGIValue(fv) 3135 self.filterspec[name] = handleListCGIValue(fv)
3121 else: 3136 else:
3122 self.filterspec[name] = fv.value 3137 self.filterspec[name] = fv.value
3123 self.filterspec = security.filterFilterspec(userid, self.classname, 3138 self.filterspec = security.filterFilterspec(userid, self.classname,
3124 self.filterspec) 3139 self.filterspec)
3125 3140
3126 # full-text search argument 3141 # full-text search argument
3127 self.search_text = None 3142 self.search_text = None
3128 for name in ':search_text @search_text'.split(): 3143 for name in ':search_text @search_text'.split():
3129 if self._form_has_key(name): 3144 if self._form_has_key(name):
3217 startwith: %(startwith)r 3232 startwith: %(startwith)r
3218 env: %(env)s 3233 env: %(env)s
3219 """ % d 3234 """ % d
3220 3235
3221 def indexargs_form(self, columns=1, sort=1, group=1, filter=1, 3236 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
3222 filterspec=1, search_text=1, exclude=[]): 3237 filterspec=1, search_text=1, exclude=[]):
3223 """ return the current index args as form elements 3238 """ return the current index args as form elements
3224 3239
3225 This routine generates an html form with hidden elements. 3240 This routine generates an html form with hidden elements.
3226 If you want to have visible form elements in your tal/jinja 3241 If you want to have visible form elements in your tal/jinja
3227 generated templates use the exclude aray to list the names for 3242 generated templates use the exclude aray to list the names for
3228 these elements. This wll prevent the function from creating 3243 these elements. This wll prevent the function from creating
3229 these elements in its output. 3244 these elements in its output.
3230 """ 3245 """
3231 l = [] 3246 html = []
3232 sc = self.special_char 3247 sc = self.special_char
3233 3248
3234 def add(k, v): 3249 def add(k, v):
3235 l.append(self.input(type="hidden", name=k, value=v)) 3250 html.append(self.input(type="hidden", name=k, value=v))
3236 if columns and self.columns: 3251 if columns and self.columns:
3237 add(sc+'columns', ','.join(self.columns)) 3252 add(sc+'columns', ','.join(self.columns))
3238 if sort: 3253 if sort:
3239 val = [] 3254 val = []
3240 for dir, attr in self.sort: 3255 for dir, attr in self.sort:
3260 continue 3275 continue
3261 if isinstance(v, list): 3276 if isinstance(v, list):
3262 # id's are stored as strings but should be treated 3277 # id's are stored as strings but should be treated
3263 # as integers in lists. 3278 # as integers in lists.
3264 if (isinstance(cls.get_transitive_prop(k), hyperdb.String) 3279 if (isinstance(cls.get_transitive_prop(k), hyperdb.String)
3265 and k != 'id'): 3280 and k != 'id'):
3266 add(k, ' '.join(v)) 3281 add(k, ' '.join(v))
3267 else: 3282 else:
3268 add(k, ','.join(v)) 3283 add(k, ','.join(v))
3269 else: 3284 else:
3270 add(k, v) 3285 add(k, v)
3271 if search_text and self.search_text: 3286 if search_text and self.search_text:
3272 add(sc+'search_text', self.search_text) 3287 add(sc+'search_text', self.search_text)
3273 add(sc+'pagesize', self.pagesize) 3288 add(sc+'pagesize', self.pagesize)
3274 add(sc+'startwith', self.startwith) 3289 add(sc+'startwith', self.startwith)
3275 return '\n'.join(l) 3290 return '\n'.join(html)
3276 3291
3277 def indexargs_url(self, url, args): 3292 def indexargs_url(self, url, args):
3278 """ Embed the current index args in a URL 3293 """ Embed the current index args in a URL
3279 3294
3280 If the value of an arg (in args dict) is None, 3295 If the value of an arg (in args dict) is None,
3368 """ 3383 """
3369 check = self._client.db.security.hasPermission 3384 check = self._client.db.security.hasPermission
3370 userid = self._client.userid 3385 userid = self._client.userid
3371 if not check('Web Access', userid): 3386 if not check('Web Access', userid):
3372 return Batch(self.client, [], self.pagesize, self.startwith, 3387 return Batch(self.client, [], self.pagesize, self.startwith,
3373 classname=self.classname) 3388 classname=self.classname)
3374 3389
3375 filterspec = self.filterspec 3390 filterspec = self.filterspec
3376 sort = self.sort 3391 sort = self.sort
3377 group = self.group 3392 group = self.group
3378 3393
3396 )], klass) 3411 )], klass)
3397 else: 3412 else:
3398 matches = None 3413 matches = None
3399 3414
3400 # filter for visibility 3415 # filter for visibility
3401 l = [id for id in klass.filter(matches, filterspec, sort, group) 3416 allowed = [id for id in klass.filter(matches, filterspec, sort, group)
3402 if check(permission, userid, self.classname, itemid=id)] 3417 if check(permission, userid, self.classname, itemid=id)]
3403 3418
3404 # return the batch object, using IDs only 3419 # return the batch object, using IDs only
3405 return Batch(self.client, l, self.pagesize, self.startwith, 3420 return Batch(self.client, allowed, self.pagesize, self.startwith,
3406 classname=self.classname) 3421 classname=self.classname)
3407 3422
3408 3423
3409 # extend the standard ZTUtils Batch object to remove dependency on 3424 # extend the standard ZTUtils Batch object to remove dependency on
3410 # Acquisition and add a couple of useful methods 3425 # Acquisition and add a couple of useful methods
3411 class Batch(ZTUtils.Batch): 3426 class Batch(ZTUtils.Batch):
3431 the batch. 3446 the batch.
3432 3447
3433 "sequence_length" is the length of the original, unbatched, sequence. 3448 "sequence_length" is the length of the original, unbatched, sequence.
3434 """ 3449 """
3435 def __init__(self, client, sequence, size, start, end=0, orphan=0, 3450 def __init__(self, client, sequence, size, start, end=0, orphan=0,
3436 overlap=0, classname=None): 3451 overlap=0, classname=None):
3437 self.client = client 3452 self.client = client
3438 self.last_index = self.last_item = None 3453 self.last_index = self.last_item = None
3439 self.current_item = None 3454 self.current_item = None
3440 self.classname = classname 3455 self.classname = classname
3441 self.sequence_length = len(sequence) 3456 self.sequence_length = len(sequence)
3442 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan, 3457 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
3443 overlap) 3458 overlap)
3444 3459
3445 # overwrite so we can late-instantiate the HTMLItem instance 3460 # overwrite so we can late-instantiate the HTMLItem instance
3446 def __getitem__(self, index): 3461 def __getitem__(self, index):
3447 if index < 0: 3462 if index < 0:
3448 if index + self.end < self.first: raise IndexError(index) 3463 if index + self.end < self.first:
3464 raise IndexError(index)
3449 return self._sequence[index + self.end] 3465 return self._sequence[index + self.end]
3450 3466
3451 if index >= self.length: 3467 if index >= self.length:
3452 raise IndexError(index) 3468 raise IndexError(index)
3453 3469
3474 return 1 3490 return 1
3475 for property in properties: 3491 for property in properties:
3476 if property == 'id' or property.endswith('.id')\ 3492 if property == 'id' or property.endswith('.id')\
3477 or isinstance(self.last_item[property], list): 3493 or isinstance(self.last_item[property], list):
3478 if (str(self.last_item[property]) != 3494 if (str(self.last_item[property]) !=
3479 str(self.current_item[property])): 3495 str(self.current_item[property])):
3480 return 1 3496 return 1
3481 else: 3497 else:
3482 if (self.last_item[property]._value != 3498 if (self.last_item[property]._value !=
3483 self.current_item[property]._value): 3499 self.current_item[property]._value):
3484 return 1 3500 return 1
3485 return 0 3501 return 0
3486 3502
3487 # override these 'cos we don't have access to acquisition 3503 # override these 'cos we don't have access to acquisition
3488 def previous(self): 3504 def previous(self):
3489 if self.start == 1: 3505 if self.start == 1:
3490 return None 3506 return None
3491 return Batch(self.client, self._sequence, self.size, 3507 return Batch(self.client, self._sequence, self.size,
3492 self.first - self._size + self.overlap, 0, self.orphan, 3508 self.first - self._size + self.overlap, 0, self.orphan,
3493 self.overlap) 3509 self.overlap)
3494 3510
3495 def next(self): 3511 def next(self):
3496 try: 3512 try:
3497 self._sequence[self.end] 3513 self._sequence[self.end]
3498 except IndexError: 3514 except IndexError:
3499 return None 3515 return None
3500 return Batch(self.client, self._sequence, self.size, 3516 return Batch(self.client, self._sequence, self.size,
3501 self.end - self.overlap, 0, self.orphan, self.overlap) 3517 self.end - self.overlap, 0, self.orphan, self.overlap)
3502 3518
3503 3519
3504 class TemplatingUtils: 3520 class TemplatingUtils:
3505 """ Utilities for templating 3521 """ Utilities for templating
3506 """ 3522 """
3507 def __init__(self, client): 3523 def __init__(self, client):
3508 self.client = client 3524 self.client = client
3509 3525
3510 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0): 3526 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
3511 return Batch(self.client, sequence, size, start, end, orphan, 3527 return Batch(self.client, sequence, size, start, end, orphan,
3512 overlap) 3528 overlap)
3513 3529
3514 def anti_csrf_nonce(self, lifetime=None): 3530 def anti_csrf_nonce(self, lifetime=None):
3515 return anti_csrf_nonce(self.client, lifetime=lifetime) 3531 return anti_csrf_nonce(self.client, lifetime=lifetime)
3516 3532
3517 def timestamp(self): 3533 def timestamp(self):
3600 res.append('<table class="calendar"><tr><td>') 3616 res.append('<table class="calendar"><tr><td>')
3601 res.append(' <table width="100%" class="calendar_nav"><tr>') 3617 res.append(' <table width="100%" class="calendar_nav"><tr>')
3602 link = "&display=%s" % date_prev_month 3618 link = "&display=%s" % date_prev_month
3603 if date_prev_month: 3619 if date_prev_month:
3604 res.append(' <td><a href="%s&display=%s">&lt;</a></td>' 3620 res.append(' <td><a href="%s&display=%s">&lt;</a></td>'
3605 % (base_link, date_prev_month)) 3621 % (base_link, date_prev_month))
3606 else: 3622 else:
3607 res.append(' <td></td>') 3623 res.append(' <td></td>')
3608 res.append(' <td>%s</td>' % calendar.month_name[display.month]) 3624 res.append(' <td>%s</td>' % calendar.month_name[display.month])
3609 if date_next_month: 3625 if date_next_month:
3610 res.append(' <td><a href="%s&display=%s">&gt;</a></td>' 3626 res.append(' <td><a href="%s&display=%s">&gt;</a></td>'
3611 % (base_link, date_next_month)) 3627 % (base_link, date_next_month))
3612 else: 3628 else:
3613 res.append(' <td></td>') 3629 res.append(' <td></td>')
3614 # spacer 3630 # spacer
3615 res.append(' <td width="100%"></td>') 3631 res.append(' <td width="100%"></td>')
3616 # year 3632 # year
3617 if date_prev_year: 3633 if date_prev_year:
3618 res.append(' <td><a href="%s&display=%s">&lt;</a></td>' 3634 res.append(' <td><a href="%s&display=%s">&lt;</a></td>'
3619 % (base_link, date_prev_year)) 3635 % (base_link, date_prev_year))
3620 else: 3636 else:
3621 res.append(' <td></td>') 3637 res.append(' <td></td>')
3622 res.append(' <td>%s</td>' % display.year) 3638 res.append(' <td>%s</td>' % display.year)
3623 if date_next_year: 3639 if date_next_year:
3624 res.append(' <td><a href="%s&display=%s">&gt;</a></td>' 3640 res.append(' <td><a href="%s&display=%s">&gt;</a></td>'
3625 % (base_link, date_next_year)) 3641 % (base_link, date_next_year))
3626 else: 3642 else:
3627 res.append(' <td></td>') 3643 res.append(' <td></td>')
3628 res.append(' </tr></table>') 3644 res.append(' </tr></table>')
3629 res.append(' </td></tr>') 3645 res.append(' </td></tr>')
3630 3646

Roundup Issue Tracker: http://roundup-tracker.org/