Mercurial > p > roundup > code
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 |
