comparison roundup/hyperdb.py @ 6148:8497bf3f23a1

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.
author Ralf Schlatterbeck <rsc@runtux.com>
date Wed, 29 Apr 2020 16:30:27 +0200
parents cf800f1febe6
children a701c9c81597
comparison
equal deleted inserted replaced
6147:f35ca71c9f2e 6148:8497bf3f23a1
43 """A roundup property type.""" 43 """A roundup property type."""
44 def __init__(self, required=False, default_value=None, quiet=False): 44 def __init__(self, required=False, default_value=None, quiet=False):
45 self.required = required 45 self.required = required
46 self.__default_value = default_value 46 self.__default_value = default_value
47 self.quiet = quiet 47 self.quiet = quiet
48 # We do not allow updates if self.computed is True
49 # For now only Multilinks (using the rev_multilink) can be computed
50 self.computed = False
48 51
49 def __repr__(self): 52 def __repr__(self):
50 ' more useful for dumps ' 53 ' more useful for dumps '
51 return '<%s.%s>' % (self.__class__.__module__, self.__class__.__name__) 54 return '<%s.%s>' % (self.__class__.__module__, self.__class__.__name__)
52 55
53 def get_default_value(self): 56 def get_default_value(self):
54 """The default value when creating a new instance of this property.""" 57 """The default value when creating a new instance of this property."""
55 return self.__default_value 58 return self.__default_value
59
60 def register (self, cls, propname):
61 """Register myself to the class of which we are a property
62 the given propname is the name we have in our class.
63 """
64 assert not getattr(self, 'cls', None)
65 self.name = propname
66 self.cls = cls
56 67
57 def sort_repr(self, cls, val, name): 68 def sort_repr(self, cls, val, name):
58 """Representation used for sorting. This should be a python 69 """Representation used for sorting. This should be a python
59 built-in type, otherwise sorting will take ages. Note that 70 built-in type, otherwise sorting will take ages. Note that
60 individual backends may chose to use something different for 71 individual backends may chose to use something different for
162 class _Pointer(_Type): 173 class _Pointer(_Type):
163 """An object designating a Pointer property that links or multilinks 174 """An object designating a Pointer property that links or multilinks
164 to a node in a specified class.""" 175 to a node in a specified class."""
165 def __init__(self, classname, do_journal='yes', try_id_parsing='yes', 176 def __init__(self, classname, do_journal='yes', try_id_parsing='yes',
166 required=False, default_value=None, 177 required=False, default_value=None,
167 msg_header_property=None, quiet=False): 178 msg_header_property=None, quiet=False, rev_multilink=None):
168 """ Default is to journal link and unlink events. 179 """ Default is to journal link and unlink events.
169 When try_id_parsing is false, we don't allow IDs in input 180 When try_id_parsing is false, we don't allow IDs in input
170 fields (the key of the Link or Multilink property must be 181 fields (the key of the Link or Multilink property must be
171 given instead). This is useful when the name of a property 182 given instead). This is useful when the name of a property
172 can be numeric. It will only work if the linked item has a 183 can be numeric. It will only work if the linked item has a
180 user mail-filtering of issue-emails for which they're 191 user mail-filtering of issue-emails for which they're
181 responsible. In that case setting 192 responsible. In that case setting
182 'msg_header_property="username"' for the assigned_to 193 'msg_header_property="username"' for the assigned_to
183 property will generated message headers of the form: 194 property will generated message headers of the form:
184 'X-Roundup-issue-assigned_to: joe_user'. 195 'X-Roundup-issue-assigned_to: joe_user'.
196 The rev_multilink is used to inject a reverse multilink into
197 the Class linked by a Link or Multilink property. Note that
198 the result is always a Multilink. The name given with
199 rev_multilink is the name in the class where it is injected.
185 """ 200 """
186 super(_Pointer, self).__init__(required, default_value, quiet) 201 super(_Pointer, self).__init__(required, default_value, quiet)
187 self.classname = classname 202 self.classname = classname
188 self.do_journal = do_journal == 'yes' 203 self.do_journal = do_journal == 'yes'
189 self.try_id_parsing = try_id_parsing == 'yes' 204 self.try_id_parsing = try_id_parsing == 'yes'
190 self.msg_header_property = msg_header_property 205 self.msg_header_property = msg_header_property
206 self.rev_multilink = rev_multilink
191 207
192 def __repr__(self): 208 def __repr__(self):
193 """more useful for dumps. But beware: This is also used in schema 209 """more useful for dumps. But beware: This is also used in schema
194 storage in SQL backends! 210 storage in SQL backends!
195 """ 211 """
225 241
226 "classname" indicates the class to link to 242 "classname" indicates the class to link to
227 243
228 "do_journal" indicates whether the linked-to nodes should have 244 "do_journal" indicates whether the linked-to nodes should have
229 'link' and 'unlink' events placed in their journal 245 'link' and 'unlink' events placed in their journal
246 "rev_property" is used when injecting reverse multilinks. By
247 default (for a normal multilink) the table name is
248 <name_of_linking_class>_<name_of_link_property>
249 e.g. for the messages multilink in issue in the
250 classic schema it would be "issue_messages". The
251 multilink table in that case has two columns, the
252 nodeid contains the ID of the linking class while
253 the linkid contains the ID of the linked-to class.
254 When injecting backlinks, for a backlink resulting
255 from a Link or Multilink the table_name,
256 linkid_name, and nodeid_name must be explicitly
257 specified. So when specifying a rev_multilink
258 property for the messages attribute in the example
259 above, we would get 'issue_messages' for the
260 table_name, 'nodeid' for the linkid_name and
261 'linkid' for the nodeid_name (note the reversal).
262 For a rev_multilink resulting, e.g. from the
263 standard 'status' Link in the Class 'issue' in the
264 classic template we would set table_name to '_issue'
265 (table names in the database get a leading
266 underscore), the nodeid_name to 'status' and the
267 linkid_name to 'id'. With these settings we can use
268 the standard query engine (with minor modifications
269 for the computed names) to resolve reverse
270 multilinks.
230 """ 271 """
231 272
232 def __init__(self, classname, do_journal='yes', required=False, 273 def __init__(self, classname, do_journal='yes', required=False,
233 quiet=False, try_id_parsing='yes'): 274 quiet=False, try_id_parsing='yes', rev_multilink=None,
275 rev_property=None):
234 276
235 super(Multilink, self).__init__(classname, 277 super(Multilink, self).__init__(classname,
236 do_journal, 278 do_journal,
237 required=required, 279 required=required,
238 default_value=[], quiet=quiet, 280 default_value=[], quiet=quiet,
239 try_id_parsing=try_id_parsing) 281 try_id_parsing=try_id_parsing,
282 rev_multilink=rev_multilink)
283 self.rev_property = rev_property
284 self.rev_classname = None
285 self.rev_propname = None
286 self.table_name = None # computed in 'register' below
287 self.linkid_name = 'linkid'
288 self.nodeid_name = 'nodeid'
289 if self.rev_property:
290 # Do not allow updates if this is a reverse multilink
291 self.computed = True
292 self.rev_classname = rev_property.cls.classname
293 self.rev_propname = rev_property.name
294 if isinstance(self.rev_property, Link):
295 self.table_name = '_' + self.rev_classname
296 self.linkid_name = 'id'
297 self.nodeid_name = '_' + self.rev_propname
298 else:
299 self.table_name = self.rev_classname + '_' + self.rev_propname
300 self.linkid_name = 'nodeid'
301 self.nodeid_name = 'linkid'
240 302
241 def from_raw(self, value, db, klass, propname, itemid, **kw): 303 def from_raw(self, value, db, klass, propname, itemid, **kw):
242 if not value: 304 if not value:
243 return [] 305 return []
244 306
307 # unnecessary :( 369 # unnecessary :(
308 value = [int(x) for x in value] 370 value = [int(x) for x in value]
309 value.sort() 371 value.sort()
310 value = [str(x) for x in value] 372 value = [str(x) for x in value]
311 return value 373 return value
374
375 def register(self, cls, propname):
376 super(Multilink, self).register(cls, propname)
377 if self.table_name is None:
378 self.table_name = self.cls.classname + '_' + self.name
312 379
313 def sort_repr(self, cls, val, name): 380 def sort_repr(self, cls, val, name):
314 if not val: 381 if not val:
315 return val 382 return val
316 op = cls.labelprop() 383 op = cls.labelprop()
508 exact_match_spec = {} 575 exact_match_spec = {}
509 for p in self.children: 576 for p in self.children:
510 if 'search' in p.need_for: 577 if 'search' in p.need_for:
511 if p.children: 578 if p.children:
512 p.search(sort=False) 579 p.search(sort=False)
513 if isinstance(p.val, type([])): 580 if getattr(p.propclass,'rev_property',None):
581 pn = p.propclass.rev_property.name
582 cl = p.propclass.rev_property.cls
583 if not isinstance(p.val, type([])):
584 p.val = [p.val]
585 if p.val == ['-1'] :
586 s1 = set(self.cls.getnodeids(retired=False))
587 s2 = set()
588 for id in cl.getnodeids(retired=False):
589 node = cl.getnode(id)
590 if node[pn]:
591 if isinstance(node [pn], type([])):
592 s2.update(node [pn])
593 else:
594 s2.add(node [pn])
595 items = s1.difference(s2)
596 elif isinstance(p.propclass.rev_property, Link):
597 items = set(cl.get(x, pn) for x in p.val)
598 else:
599 items = set().union(*(cl.get(x, pn) for x in p.val))
600 filterspec[p.name] = list(sorted(items))
601 elif isinstance(p.val, type([])):
514 exact = [] 602 exact = []
515 subst = [] 603 subst = []
516 for v in p.val: 604 for v in p.val:
517 if isinstance(v, Exact_Match): 605 if isinstance(v, Exact_Match):
518 exact.append(v.value) 606 exact.append(v.value)
718 """Error to be raised when there is some problem in the database code 806 """Error to be raised when there is some problem in the database code
719 """ 807 """
720 pass 808 pass
721 809
722 810
723 class Database: 811 class Database(object):
724 """A database for storing records containing flexible data types. 812 """A database for storing records containing flexible data types.
725 813
726 This class defines a hyperdatabase storage layer, which the Classes use to 814 This class defines a hyperdatabase storage layer, which the Classes use to
727 store their data. 815 store their data.
728 816
770 raise NotImplementedError 858 raise NotImplementedError
771 859
772 def post_init(self): 860 def post_init(self):
773 """Called once the schema initialisation has finished. 861 """Called once the schema initialisation has finished.
774 If 'refresh' is true, we want to rebuild the backend 862 If 'refresh' is true, we want to rebuild the backend
775 structures. 863 structures. Note that post_init can be called multiple times,
776 """ 864 at least during regression testing.
777 raise NotImplementedError 865 """
866 done = getattr(self, 'post_init_done', None)
867 for cn in self.getclasses():
868 cl = self.getclass(cn)
869 for p in cl.properties:
870 prop = cl.properties[p]
871 if not isinstance (prop, (Link, Multilink)):
872 continue
873 if prop.rev_multilink:
874 linkcls = self.getclass(prop.classname)
875 if prop.rev_multilink in linkcls.properties:
876 if not done:
877 raise ValueError(
878 "%s already a property of class %s"%
879 (prop.rev_multilink, linkcls.classname))
880 else:
881 linkcls.properties[prop.rev_multilink] = Multilink(
882 cl.classname, rev_property=prop)
883 self.post_init_done = True
778 884
779 def refresh_database(self): 885 def refresh_database(self):
780 """Called to indicate that the backend should rebuild all tables 886 """Called to indicate that the backend should rebuild all tables
781 and structures. Not called in normal usage.""" 887 and structures. Not called in normal usage."""
782 raise NotImplementedError 888 raise NotImplementedError
941 raise ValueError('"creation", "activity", "creator" and ' 1047 raise ValueError('"creation", "activity", "creator" and '
942 '"actor" are reserved') 1048 '"actor" are reserved')
943 1049
944 self.classname = classname 1050 self.classname = classname
945 self.properties = properties 1051 self.properties = properties
1052 # Make the class and property name known to the property
1053 for p in properties:
1054 properties[p].register(self, p)
946 self.db = weakref.proxy(db) # use a weak ref to avoid circularity 1055 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
947 self.key = '' 1056 self.key = ''
948 1057
949 # should we journal changes (default yes) 1058 # should we journal changes (default yes)
950 self.do_journal = 1 1059 self.do_journal = 1

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