Skip to content

Commit 41b16fa

Browse files
committed
Generate savepoint only if necessary
Now some methods got an additional 'allow_abort' parameter. By default this is True. When False the postgres backend generates a savepoint. The methods are called with allow_abort=False from some of the cgi methods which can produce a traceback when called with data from the web-interface.
1 parent 02b65d7 commit 41b16fa

File tree

7 files changed

+67
-33
lines changed

7 files changed

+67
-33
lines changed

roundup/backends/back_anydbm.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -381,11 +381,13 @@ def savenode(self, classname, nodeid, node):
381381
'save %s%s %r' % (classname, nodeid, node))
382382
self.transactions.append((self.doSaveNode, (classname, nodeid, node)))
383383

384-
def getnode(self, classname, nodeid, db=None, cache=1):
384+
def getnode(self, classname, nodeid, db=None, cache=1, allow_abort=True):
385385
""" get a node from the database
386386
387387
Note the "cache" parameter is not used, and exists purely for
388388
backward compatibility!
389+
390+
'allow_abort' is used only in sql backends.
389391
"""
390392
# try the cache
391393
cache_dict = self.cache.setdefault(classname, {})
@@ -1026,7 +1028,7 @@ def create_inner(self, **propvalues):
10261028

10271029
return newid
10281030

1029-
def get(self, nodeid, propname, default=_marker, cache=1):
1031+
def get(self, nodeid, propname, default=_marker, cache=1, allow_abort=True):
10301032
"""Get the value of a property on an existing node of this class.
10311033
10321034
'nodeid' must be the id of an existing node of this class or an
@@ -1035,6 +1037,8 @@ def get(self, nodeid, propname, default=_marker, cache=1):
10351037
10361038
'cache' exists for backward compatibility, and is not used.
10371039
1040+
'allow_abort' is used only in sql backends.
1041+
10381042
Attempts to get the "creation" or "activity" properties should
10391043
do the right thing.
10401044
"""
@@ -1459,8 +1463,9 @@ def restore(self, nodeid):
14591463

14601464
self.fireReactors('restore', nodeid, None)
14611465

1462-
def is_retired(self, nodeid, cldb=None):
1466+
def is_retired(self, nodeid, cldb=None, allow_abort=True):
14631467
"""Return true if the node is retired.
1468+
'allow_abort' is used only in sql backends.
14641469
"""
14651470
node = self.db.getnode(self.classname, nodeid, cldb)
14661471
if self.db.RETIRED_FLAG in node:

roundup/backends/back_postgresql.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -560,14 +560,17 @@ def clear(self):
560560
self.cursor.execute('DROP SEQUENCE _%s_ids' % cn)
561561
self.cursor.execute('CREATE SEQUENCE _%s_ids' % cn)
562562

563-
def getnode (self, classname, nodeid, fetch_multilinks=True):
563+
def getnode(self, classname, nodeid, fetch_multilinks=True,
564+
allow_abort=True):
564565
""" For use of savepoint see 'Class' below """
565-
self.sql('savepoint sp')
566+
if not allow_abort:
567+
self.sql('savepoint sp')
566568
try:
567569
getnode = rdbms_common.Database.getnode
568570
return getnode(self, classname, nodeid, fetch_multilinks)
569571
except psycopg2.errors.DataError as err:
570-
self.sql('rollback to savepoint sp')
572+
if not allow_abort:
573+
self.sql('rollback to savepoint sp')
571574
raise hyperdb.HyperdbValueError(str (err).split('\n')[0])
572575

573576

@@ -583,28 +586,26 @@ class Class(PostgresqlClass, rdbms_common.Class):
583586
"""
584587

585588
def filter(self, *args, **kw):
586-
self.db.sql('savepoint sp')
587589
try:
588590
return rdbms_common.Class.filter(self, *args, **kw)
589591
except psycopg2.errors.DataError as err:
590-
self.db.sql('rollback to savepoint sp')
591592
raise hyperdb.HyperdbValueError(str (err).split('\n')[0])
592593

593594
def filter_iter(self, *args, **kw):
594-
self.db.sql('savepoint sp')
595595
try:
596596
for v in rdbms_common.Class.filter_iter(self, *args, **kw):
597597
yield v
598598
except psycopg2.errors.DataError as err:
599-
self.db.sql('rollback to savepoint sp')
600599
raise hyperdb.HyperdbValueError(str (err).split('\n')[0])
601600

602-
def is_retired(self, nodeid):
603-
self.db.sql('savepoint sp')
601+
def is_retired(self, nodeid, allow_abort=True):
602+
if not allow_abort:
603+
self.db.sql('savepoint sp')
604604
try:
605605
return rdbms_common.Class.is_retired(self, nodeid)
606606
except psycopg2.errors.DataError as err:
607-
self.db.sql('rollback to savepoint sp')
607+
if not allow_abort:
608+
self.db.sql('rollback to savepoint sp')
608609
raise hyperdb.HyperdbValueError (str (err).split('\n')[0])
609610

610611

roundup/backends/rdbms_common.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#
1+
22
# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
33
# This module is free software, and you may redistribute it and/or modify
44
# under the same terms as Python, so long as this copyright message and
@@ -1251,11 +1251,14 @@ def _materialize_multilinks(self, classname, nodeid, node, props=None):
12511251
if propname not in node:
12521252
self._materialize_multilink(classname, nodeid, node, propname)
12531253

1254-
def getnode(self, classname, nodeid, fetch_multilinks=True):
1254+
def getnode(self, classname, nodeid, fetch_multilinks=True,
1255+
allow_abort=True):
12551256
""" Get a node from the database.
12561257
For optimisation optionally we don't fetch multilinks
12571258
(lazy Multilinks).
12581259
But for internal database operations we need them.
1260+
'allow_abort' determines if we allow that the current
1261+
transaction is aborted due to a data error (e.g. invalid nodeid).
12591262
"""
12601263
# see if we have this node cached
12611264
key = (classname, nodeid)
@@ -1863,20 +1866,24 @@ def create_inner(self, **propvalues):
18631866
# XXX numeric ids
18641867
return str(newid)
18651868

1866-
def get(self, nodeid, propname, default=_marker, cache=1):
1869+
def get(self, nodeid, propname, default=_marker, cache=1, allow_abort=True):
18671870
"""Get the value of a property on an existing node of this class.
18681871
18691872
'nodeid' must be the id of an existing node of this class or an
18701873
IndexError is raised. 'propname' must be the name of a property
18711874
of this class or a KeyError is raised.
18721875
18731876
'cache' exists for backwards compatibility, and is not used.
1877+
1878+
'allow_abort' determines if we allow that the current
1879+
transaction is aborted due to a data error (e.g. invalid nodeid).
18741880
"""
18751881
if propname == 'id':
18761882
return nodeid
18771883

18781884
# get the node's dict
1879-
d = self.db.getnode(self.classname, nodeid, fetch_multilinks=False)
1885+
d = self.db.getnode(self.classname, nodeid, fetch_multilinks=False,
1886+
allow_abort=allow_abort)
18801887
# handle common case -- that property is in dict -- first
18811888
# if None and one of creator/creation actor/activity return None
18821889
if propname in d:
@@ -2245,8 +2252,10 @@ def restore(self, nodeid):
22452252

22462253
self.fireReactors('restore', nodeid, None)
22472254

2248-
def is_retired(self, nodeid):
2255+
def is_retired(self, nodeid, allow_abort=True):
22492256
"""Return true if the node is retired
2257+
The allow_abort parameter determines if we allow the
2258+
transaction to be aborted when an invalid nodeid has been passed.
22502259
"""
22512260
# Do not produce invalid sql, the id must be numeric
22522261
try:

roundup/cgi/actions.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -661,8 +661,13 @@ def _editnodes(self, all_props, all_links):
661661
cn, nodeid = needed
662662
if props:
663663
if nodeid is not None and int(nodeid) > 0:
664-
# make changes to the node
665-
props = self._changenode(cn, nodeid, props)
664+
# make changes to the node, if an error occurs the
665+
# db may be in a state that needs rollback
666+
try:
667+
props = self._changenode(cn, nodeid, props)
668+
except (IndexError, ValueError):
669+
self.db.rollback ()
670+
raise
666671

667672
# and some nice feedback for the user
668673
if props:

roundup/cgi/templating.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ def lookupKeys(linkcl, key, ids, num_re=num_re):
587587
for entry in ids:
588588
if num_re.match(entry):
589589
try:
590-
label = linkcl.get(entry, key)
590+
label = linkcl.get(entry, key, allow_abort=False)
591591
except IndexError:
592592
# fall back to id if illegal (avoid template crash)
593593
label = entry
@@ -1121,7 +1121,8 @@ def __getitem__(self, item):
11211121
value = None
11221122
try:
11231123
if int(self._nodeid) > 0:
1124-
value = self._klass.get(self._nodeid, items[0], None)
1124+
value = self._klass.get(self._nodeid, items[0], None,
1125+
allow_abort=False)
11251126
except (IndexError, ValueError):
11261127
value = self._nodeid
11271128
if value is None:
@@ -1157,7 +1158,7 @@ def designator(self):
11571158

11581159
def is_retired(self):
11591160
"""Is this item retired?"""
1160-
return self._klass.is_retired(self._nodeid)
1161+
return self._klass.is_retired(self._nodeid, allow_abort=False)
11611162

11621163
def submit(self, label=''"Submit Changes", action="edit", html_kwargs={}):
11631164
"""Generate a submit button.
@@ -2572,7 +2573,7 @@ def field(self, showid=0, size=None, **kwargs):
25722573
idparse = self._prop.try_id_parsing
25732574
if k and num_re.match(self._value):
25742575
try:
2575-
value = linkcl.get(self._value, k)
2576+
value = linkcl.get(self._value, k, allow_abort=False)
25762577
except (IndexError, hyperdb.HyperdbValueError) as err:
25772578
if idparse:
25782579
self._client.add_error_message(str(err))
@@ -3043,7 +3044,7 @@ def make_key_function(db, classname, sort_on=None):
30433044

30443045
def keyfunc(a):
30453046
if num_re.match(a):
3046-
a = linkcl.get(a, sort_on)
3047+
a = linkcl.get(a, sort_on, allow_abort=False)
30473048
# In Python3 we may not compare numbers/strings and None
30483049
if a is None:
30493050
if isinstance(prop, (hyperdb.Number, hyperdb.Integer)):

roundup/hyperdb.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,10 +1065,12 @@ def unserialise(self, classname, node):
10651065
"""
10661066
return node
10671067

1068-
def getnode(self, classname, nodeid):
1068+
def getnode(self, classname, nodeid, allow_abort=True):
10691069
"""Get a node from the database.
10701070
10711071
'cache' exists for backwards compatibility, and is not used.
1072+
'allow_abort' determines if we allow that the current
1073+
transaction is aborted due to a data error (e.g. invalid nodeid).
10721074
"""
10731075
raise NotImplementedError
10741076

@@ -1235,14 +1237,16 @@ def create(self, **propvalues):
12351237
"""
12361238
raise NotImplementedError
12371239

1238-
def get(self, nodeid, propname, default=_marker, cache=1):
1240+
def get(self, nodeid, propname, default=_marker, cache=1, allow_abort=True):
12391241
"""Get the value of a property on an existing node of this class.
12401242
12411243
'nodeid' must be the id of an existing node of this class or an
12421244
IndexError is raised. 'propname' must be the name of a property
12431245
of this class or a KeyError is raised.
12441246
12451247
'cache' exists for backwards compatibility, and is not used.
1248+
'allow_abort' determines if we allow that the current
1249+
transaction is aborted due to a data error (e.g. invalid nodeid).
12461250
"""
12471251
raise NotImplementedError
12481252

@@ -1300,8 +1304,10 @@ def restore(self, nodeid):
13001304
"""
13011305
raise NotImplementedError
13021306

1303-
def is_retired(self, nodeid):
1307+
def is_retired(self, nodeid, allow_abort=True):
13041308
"""Return true if the node is rerired
1309+
'allow_abort' specifies if we allow the transaction to be
1310+
aborted if a syntactically invalid nodeid is passed.
13051311
"""
13061312
raise NotImplementedError
13071313

@@ -2182,10 +2188,13 @@ def export_files(self, dirname, nodeid):
21822188
ensureParentsExist(dest)
21832189
shutil.copyfile(source, dest)
21842190

2185-
def get(self, nodeid, propname, default=_marker, cache=1):
2191+
def get(self, nodeid, propname, default=_marker, cache=1, allow_abort=True):
21862192
""" Trap the content propname and get it from the file
21872193
21882194
'cache' exists for backwards compatibility, and is not used.
2195+
2196+
'allow_abort' determines if we allow that the current
2197+
transaction is aborted due to a data error (e.g. invalid nodeid).
21892198
"""
21902199
poss_msg = 'Possibly an access right configuration problem.'
21912200
if propname == 'content':
@@ -2212,9 +2221,11 @@ def get(self, nodeid, propname, default=_marker, cache=1):
22122221
return self.db.getfile(self.classname, nodeid, None)
22132222

22142223
if default is not _marker:
2215-
return self.subclass.get(self, nodeid, propname, default)
2224+
return self.subclass.get(self, nodeid, propname, default,
2225+
allow_abort=allow_abort)
22162226
else:
2217-
return self.subclass.get(self, nodeid, propname)
2227+
return self.subclass.get(self, nodeid, propname,
2228+
allow_abort=allow_abort)
22182229

22192230
def import_files(self, dirname, nodeid):
22202231
""" Import the "content" property as a file

test/test_templating.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ def test_HTMLDatabase_list(self):
143143
db = MockNull(issue = HTMLDatabase(self.client).issue)
144144
db.issue._klass.list = lambda : ['23', 'a', 'b']
145145
# Make db.getclass return something that has a sensible 'get' method
146-
mock = MockNull(get = lambda x, y : None)
146+
def get(x, y, allow_abort=True):
147+
return None
148+
mock = MockNull(get = get)
147149
db.issue._db.getclass = lambda x : mock
148150
l = db.issue.list()
149151

@@ -170,7 +172,7 @@ def lookup(key):
170172

171173
def test_lookupKeys(self):
172174
db = HTMLDatabase(self.client)
173-
def get(entry, key):
175+
def get(entry, key, allow_abort=True):
174176
return {'1': 'green', '2': 'eggs'}.get(entry, entry)
175177
shrubbery = MockNull(get=get)
176178
db._db.classes = {'shrubbery': shrubbery}

0 commit comments

Comments
 (0)