Skip to content

Commit 6ac6d34

Browse files
committed
Allow to define reverse Multilinks
Now it's possible to specify a rev_multilink parameter when creating Link or Multilink properties. The parameter takes a property name to be inserted into the linked-to class. It allows to navigate from the other side of the link as if it where a forward Multilink using the existing data structures.
1 parent 0329681 commit 6ac6d34

File tree

10 files changed

+266
-52
lines changed

10 files changed

+266
-52
lines changed

CHANGES.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ Features:
7474
with new RoundupException (inheriting from Exception) for most
7575
roundup exceptions. (John Rouillard and Ralf Schlatterbeck on
7676
request from Robert Klonner.)
77+
- When defining Link or Multilink properties in the schema, it's now
78+
possible to add a parameter rev_multilink that accepts a property name
79+
to be inserted into the linked-to class. So this creates a reverse
80+
Multilink property in the linked-to class. This Multilink is read-only
81+
(cannot be updated) but can be used in filter -- and thus in normal
82+
index templates as well as in the REST and XMLRPC APIs. (Ralf
83+
Schlatterbeck)
7784

7885
Fixed:
7986

doc/customizing.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,14 @@ behaviour:
716716
(like, e.g., product codes). For these roundup needs to match the
717717
numeric name and should never match an ID. In this case you can set
718718
``try_id_parsing='no'``.
719+
- The ``rev_multilink`` option takes a property name to be inserted
720+
into the linked-to class. This property is a Multilink property that
721+
links back to the current class. The new Multilink is read-only (it
722+
is automagically modified if the Link or Multilink property defining
723+
it is modified). The new property can be used in normal searches
724+
using the "filter" method of the Class. This means it can be used
725+
like other Multilink properties when searching (in an index
726+
template) or via the REST- and XMLRPC-APIs.
719727
- The ``msg_header_property`` is used by the mail gateway when sending
720728
out messages. When a link or multilink property of an issue changes,
721729
roundup creates email headers of the form::

roundup/backends/back_anydbm.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ def __init__(self, config, journaltag=None):
200200
def post_init(self):
201201
"""Called once the schema initialisation has finished.
202202
"""
203+
super(Database, self).post_init()
203204
# reindex the db if necessary
204205
if self.indexer.should_reindex():
205206
self.reindex()
@@ -915,6 +916,12 @@ def create_inner(self, **propvalues):
915916
'creation' in propvalues or 'activity' in propvalues):
916917
raise KeyError('"creator", "actor", "creation" and '
917918
'"activity" are reserved')
919+
920+
for p in propvalues:
921+
prop = self.properties[p]
922+
if prop.computed:
923+
raise KeyError('"%s" is a computed property'%p)
924+
918925
# new node's id
919926
newid = self.db.newid(self.classname)
920927

@@ -1197,6 +1204,11 @@ def set_inner(self, nodeid, **propvalues):
11971204
if 'id' in propvalues:
11981205
raise KeyError('"id" is reserved')
11991206

1207+
for p in propvalues:
1208+
prop = self.properties[p]
1209+
if prop.computed:
1210+
raise KeyError('"%s" is a computed property'%p)
1211+
12001212
if self.db.journaltag is None:
12011213
raise hyperdb.DatabaseError(_('Database open read-only'))
12021214

@@ -1556,6 +1568,8 @@ def find(self, **propspec):
15561568
15571569
db.issue.find(messages='1')
15581570
db.issue.find(messages={'1':1,'3':1}, files={'7':1})
1571+
db.issue.find(messages=('1','3'), files=('7',))
1572+
db.issue.find(messages=['1','3'], files=['7'])
15591573
"""
15601574
for propname, itemids in propspec.items():
15611575
# check the prop is OK
@@ -1574,7 +1588,10 @@ def find(self, **propspec):
15741588
continue
15751589
for propname, itemids in propspec.items():
15761590
if type(itemids) is not type({}):
1577-
itemids = {itemids:1}
1591+
if itemids is None or isinstance(itemids, type("")):
1592+
itemids = {itemids:1}
1593+
else:
1594+
itemids = dict.fromkeys(itemids)
15781595

15791596
# special case if the item doesn't have this property
15801597
if propname not in item:
@@ -1744,12 +1761,17 @@ def _filter(self, search_matches, filterspec, proptree,
17441761
u.append(entry)
17451762
l.append((LINK, k, u))
17461763
elif isinstance(propclass, hyperdb.Multilink):
1747-
# the value -1 is a special "not set" sentinel
1748-
if v in ('-1', ['-1']):
1749-
v = []
1750-
elif type(v) is not type([]):
1751-
v = [v]
1752-
l.append((MULTILINK, k, v))
1764+
# If it's a reverse multilink, we've already
1765+
# computed the ids of our own class.
1766+
if propclass.rev_property:
1767+
l.append((OTHER, 'id', v))
1768+
else:
1769+
# the value -1 is a special "not set" sentinel
1770+
if v in ('-1', ['-1']):
1771+
v = []
1772+
elif type(v) is not type([]):
1773+
v = [v]
1774+
l.append((MULTILINK, k, v))
17531775
elif isinstance(propclass, hyperdb.String) and k != 'id':
17541776
if type(v) is not type([]):
17551777
v = [v]

roundup/backends/back_mysql.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -605,12 +605,12 @@ def supports_subselects(self):
605605
# TODO: AFAIK its version dependent for MySQL
606606
return False
607607

608-
def _subselect(self, classname, multilink_table):
608+
def _subselect(self, classname, multilink_table, nodeid_name):
609609
''' "I can't believe it's not a toy RDBMS"
610610
see, even toy RDBMSes like gadfly and sqlite can do sub-selects...
611611
'''
612-
self.db.sql('select nodeid from %s'%multilink_table)
613-
s = ','.join([x[0] for x in self.db.sql_fetchall()])
612+
self.db.sql('select %s from %s'%(nodeid_name, multilink_table))
613+
s = ','.join([str(x[0]) for x in self.db.sql_fetchall()])
614614
return '_%s.id not in (%s)'%(classname, s)
615615

616616
def create_inner(self, **propvalues):

roundup/backends/blobfiles.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def files_in_dir(dir):
3434
num_files = num_files + files_in_dir(full_filename)
3535
return num_files
3636

37-
class FileStorage:
37+
class FileStorage(object):
3838
"""Store files in some directory structure
3939
4040
Some databases do not permit the storage of arbitrary data (i.e.,

roundup/backends/rdbms_common.py

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ def post_init(self):
293293
We should now confirm that the schema defined by our "classes"
294294
attribute actually matches the schema in the database.
295295
"""
296+
super(Database, self).post_init()
296297

297298
# upgrade the database for column type changes, new internal
298299
# tables, etc.
@@ -512,6 +513,10 @@ def determine_columns(self, properties):
512513
mls = []
513514
# add the multilinks separately
514515
for col, prop in properties:
516+
# Computed props are not in the db
517+
if prop.computed:
518+
continue
519+
515520
if isinstance(prop, Multilink):
516521
mls.append(col)
517522
continue
@@ -1128,8 +1133,11 @@ def _materialize_multilink(self, classname, nodeid, node, propname):
11281133
""" evaluation of single Multilink (lazy eval may have skipped this)
11291134
"""
11301135
if propname not in node:
1131-
sql = 'select linkid from %s_%s where nodeid=%s' % (
1132-
classname, propname, self.arg)
1136+
prop = self.getclass(classname).properties[propname]
1137+
tn = prop.table_name
1138+
lid = prop.linkid_name
1139+
nid = prop.nodeid_name
1140+
sql = 'select %s from %s where %s=%s' % (lid, tn, nid, self.arg)
11331141
self.sql(sql, (nodeid,))
11341142
# extract the first column from the result
11351143
# XXX numeric ids
@@ -1536,7 +1544,8 @@ def schema(self):
15361544
""" A dumpable version of the schema that we can store in the
15371545
database
15381546
"""
1539-
return (self.key, [(x, repr(y)) for x, y in self.properties.items()])
1547+
return (self.key,
1548+
[(x, repr(y)) for x, y in self.properties.items() if not y.computed])
15401549

15411550
def enableJournalling(self):
15421551
"""Turn journalling on for this class
@@ -1585,6 +1594,11 @@ def create_inner(self, **propvalues):
15851594
raise KeyError('"creator", "actor", "creation" and '
15861595
'"activity" are reserved')
15871596

1597+
for p in propvalues:
1598+
prop = self.properties[p]
1599+
if prop.computed:
1600+
raise KeyError('"%s" is a computed property'%p)
1601+
15881602
# new node's id
15891603
newid = self.db.newid(self.classname)
15901604

@@ -1814,6 +1828,11 @@ def set_inner(self, nodeid, **propvalues):
18141828
if 'id' in propvalues:
18151829
raise KeyError('"id" is reserved')
18161830

1831+
for p in propvalues:
1832+
prop = self.properties[p]
1833+
if prop.computed:
1834+
raise KeyError('"%s" is a computed property'%p)
1835+
18171836
if self.db.journaltag is None:
18181837
raise DatabaseError(_('Database open read-only'))
18191838

@@ -2312,13 +2331,13 @@ def getnodeids(self, retired=None):
23122331
ids = [str(x[0]) for x in self.db.cursor.fetchall()]
23132332
return ids
23142333

2315-
def _subselect(self, classname, multilink_table):
2334+
def _subselect(self, classname, multilink_table, nodeid_name):
23162335
"""Create a subselect. This is factored out because some
23172336
databases (hmm only one, so far) doesn't support subselects
23182337
look for "I can't believe it's not a toy RDBMS" in the mysql
23192338
backend.
23202339
"""
2321-
return '_%s.id not in (select nodeid from %s)'%(classname,
2340+
return '_%s.id not in (select %s from %s)'%(classname, nodeid_name,
23222341
multilink_table)
23232342

23242343
# Some DBs order NULL values last. Set this variable in the backend
@@ -2379,7 +2398,8 @@ def _filter_multilink_expression_fallback(
23792398
# we have ids of the classname table
23802399
return ids.where("_%s.id" % classname, self.db.arg)
23812400

2382-
def _filter_multilink_expression(self, classname, multilink_table, v):
2401+
def _filter_multilink_expression(self, classname, multilink_table,
2402+
linkid_name, nodeid_name, v):
23832403
""" Filters out elements of the classname table that do not
23842404
match the given expression.
23852405
Returns tuple of 'WHERE' introns for the overall filter.
@@ -2397,9 +2417,8 @@ def _filter_multilink_expression(self, classname, multilink_table, v):
23972417
classname, multilink_table, expr)
23982418

23992419
atom = \
2400-
"%s IN(SELECT linkid FROM %s WHERE nodeid=a.id)" % (
2401-
self.db.arg,
2402-
multilink_table)
2420+
"%s IN(SELECT %s FROM %s WHERE %s=a.id)" % (
2421+
self.db.arg, linkid_name, multilink_table, nodeid_name)
24032422

24042423
intron = \
24052424
"_%(classname)s.id in (SELECT id " \
@@ -2414,8 +2433,8 @@ def collect_values(n): values.append(n.x)
24142433
return intron, values
24152434
except:
24162435
# original behavior
2417-
where = "%s.linkid in (%s)" % (
2418-
multilink_table, ','.join([self.db.arg] * len(v)))
2436+
where = "%s.%s in (%s)" % (
2437+
multilink_table, linkid_name, ','.join([self.db.arg] * len(v)))
24192438
return where, v, True # True to indicate original
24202439

24212440
def _filter_sql (self, search_matches, filterspec, srt=[], grp=[], retr=0,
@@ -2479,34 +2498,36 @@ def _filter_sql (self, search_matches, filterspec, srt=[], grp=[], retr=0,
24792498
if isinstance(propclass, Multilink):
24802499
if 'search' in p.need_for:
24812500
mlfilt = 1
2482-
tn = '%s_%s'%(pcn, k)
2501+
tn = propclass.table_name
2502+
nid = propclass.nodeid_name
2503+
lid = propclass.linkid_name
24832504
if v in ('-1', ['-1'], []):
24842505
# only match rows that have count(linkid)=0 in the
24852506
# corresponding multilink table)
2486-
where.append(self._subselect(pcn, tn))
2507+
where.append(self._subselect(pcn, tn, nid))
24872508
else:
24882509
frum.append(tn)
24892510
gen_join = True
24902511

24912512
if p.has_values and isinstance(v, type([])):
24922513
result = self._filter_multilink_expression(pln,
2493-
tn, v)
2514+
tn, lid, nid, v)
24942515
# XXX: We dont need an id join if we used the filter
24952516
gen_join = len(result) == 3
24962517

24972518
if gen_join:
2498-
where.append('_%s.id=%s.nodeid'%(pln,tn))
2519+
where.append('_%s.id=%s.%s'%(pln, tn, nid))
24992520

25002521
if p.children:
25012522
frum.append('_%s as _%s' % (cn, ln))
2502-
where.append('%s.linkid=_%s.id'%(tn, ln))
2523+
where.append('%s.%s=_%s.id'%(tn, lid, ln))
25032524

25042525
if p.has_values:
25052526
if isinstance(v, type([])):
25062527
where.append(result[0])
25072528
args += result[1]
25082529
else:
2509-
where.append('%s.linkid=%s'%(tn, a))
2530+
where.append('%s.%s=%s'%(tn, lid, a))
25102531
args.append(v)
25112532
if 'sort' in p.need_for:
25122533
assert not p.attr_sort_done and not p.sort_ids_needed

0 commit comments

Comments
 (0)