72 except KeyError: |
72 except KeyError: |
73 for base in bases: |
73 for base in bases: |
74 etype = getattr(base, 'id', None) |
74 etype = getattr(base, 'id', None) |
75 if etype and etype != 'Any': |
75 if etype and etype != 'Any': |
76 return etype |
76 return etype |
77 |
77 |
78 def _get_defs(attr, name, bases, classdict): |
78 def _get_defs(attr, name, bases, classdict): |
79 try: |
79 try: |
80 yield name, classdict.pop(attr) |
80 yield name, classdict.pop(attr) |
81 except KeyError: |
81 except KeyError: |
82 for base in bases: |
82 for base in bases: |
84 value = getattr(base, attr) |
84 value = getattr(base, attr) |
85 delattr(base, attr) |
85 delattr(base, attr) |
86 yield base.__name__, value |
86 yield base.__name__, value |
87 except AttributeError: |
87 except AttributeError: |
88 continue |
88 continue |
89 |
89 |
90 class metaentity(type): |
90 class metaentity(type): |
91 """this metaclass sets the relation tags on the entity class |
91 """this metaclass sets the relation tags on the entity class |
92 and deals with the `widgets` attribute |
92 and deals with the `widgets` attribute |
93 """ |
93 """ |
94 def __new__(mcs, name, bases, classdict): |
94 def __new__(mcs, name, bases, classdict): |
136 |
136 |
137 |
137 |
138 class Entity(AppRsetObject, dict): |
138 class Entity(AppRsetObject, dict): |
139 """an entity instance has e_schema automagically set on |
139 """an entity instance has e_schema automagically set on |
140 the class and instances has access to their issuing cursor. |
140 the class and instances has access to their issuing cursor. |
141 |
141 |
142 A property is set for each attribute and relation on each entity's type |
142 A property is set for each attribute and relation on each entity's type |
143 class. Becare that among attributes, 'eid' is *NEITHER* stored in the |
143 class. Becare that among attributes, 'eid' is *NEITHER* stored in the |
144 dict containment (which acts as a cache for other attributes dynamically |
144 dict containment (which acts as a cache for other attributes dynamically |
145 fetched) |
145 fetched) |
146 |
146 |
149 |
149 |
150 :type rest_var: str |
150 :type rest_var: str |
151 :cvar rest_var: indicates which attribute should be used to build REST urls |
151 :cvar rest_var: indicates which attribute should be used to build REST urls |
152 If None is specified, the first non-meta attribute will |
152 If None is specified, the first non-meta attribute will |
153 be used |
153 be used |
154 |
154 |
155 :type skip_copy_for: list |
155 :type skip_copy_for: list |
156 :cvar skip_copy_for: a list of relations that should be skipped when copying |
156 :cvar skip_copy_for: a list of relations that should be skipped when copying |
157 this kind of entity. Note that some relations such |
157 this kind of entity. Note that some relations such |
158 as composite relations or relations that have '?1' as object |
158 as composite relations or relations that have '?1' as object |
159 cardinality |
159 cardinality |
160 """ |
160 """ |
161 __metaclass__ = metaentity |
161 __metaclass__ = metaentity |
162 __registry__ = 'etypes' |
162 __registry__ = 'etypes' |
163 __select__ = yes() |
163 __select__ = yes() |
164 |
164 |
165 # class attributes that must be set in class definition |
165 # class attributes that must be set in class definition |
166 id = None |
166 id = None |
167 rest_attr = None |
167 rest_attr = None |
168 fetch_attrs = None |
168 fetch_attrs = None |
169 skip_copy_for = () |
169 skip_copy_for = () |
170 # class attributes set automatically at registration time |
170 # class attributes set automatically at registration time |
171 e_schema = None |
171 e_schema = None |
172 |
172 |
173 @classmethod |
173 @classmethod |
174 def registered(cls, registry): |
174 def registered(cls, registry): |
175 """build class using descriptor at registration time""" |
175 """build class using descriptor at registration time""" |
176 assert cls.id is not None |
176 assert cls.id is not None |
177 super(Entity, cls).registered(registry) |
177 super(Entity, cls).registered(registry) |
178 if cls.id != 'Any': |
178 if cls.id != 'Any': |
179 cls.__initialize__() |
179 cls.__initialize__() |
180 return cls |
180 return cls |
181 |
181 |
182 MODE_TAGS = set(('link', 'create')) |
182 MODE_TAGS = set(('link', 'create')) |
183 CATEGORY_TAGS = set(('primary', 'secondary', 'generic', 'generated')) # , 'metadata')) |
183 CATEGORY_TAGS = set(('primary', 'secondary', 'generic', 'generated')) # , 'metadata')) |
184 @classmethod |
184 @classmethod |
185 def __initialize__(cls): |
185 def __initialize__(cls): |
186 """initialize a specific entity class by adding descriptors to access |
186 """initialize a specific entity class by adding descriptors to access |
208 attr = 'reverse_%s' % rschema.type |
208 attr = 'reverse_%s' % rschema.type |
209 setattr(cls, attr, ObjectRelation(rschema)) |
209 setattr(cls, attr, ObjectRelation(rschema)) |
210 if mixins: |
210 if mixins: |
211 cls.__bases__ = tuple(mixins + [p for p in cls.__bases__ if not p is object]) |
211 cls.__bases__ = tuple(mixins + [p for p in cls.__bases__ if not p is object]) |
212 cls.debug('plugged %s mixins on %s', mixins, etype) |
212 cls.debug('plugged %s mixins on %s', mixins, etype) |
213 |
213 |
214 @classmethod |
214 @classmethod |
215 def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X', |
215 def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X', |
216 settype=True, ordermethod='fetch_order'): |
216 settype=True, ordermethod='fetch_order'): |
217 """return a rql to fetch all entities of the class type""" |
217 """return a rql to fetch all entities of the class type""" |
218 restrictions = restriction or [] |
218 restrictions = restriction or [] |
229 rql = 'Any %s' % ','.join(selection) |
229 rql = 'Any %s' % ','.join(selection) |
230 if orderby: |
230 if orderby: |
231 rql += ' ORDERBY %s' % ','.join(orderby) |
231 rql += ' ORDERBY %s' % ','.join(orderby) |
232 rql += ' WHERE %s' % ', '.join(restrictions) |
232 rql += ' WHERE %s' % ', '.join(restrictions) |
233 return rql |
233 return rql |
234 |
234 |
235 @classmethod |
235 @classmethod |
236 def _fetch_restrictions(cls, mainvar, varmaker, fetchattrs, |
236 def _fetch_restrictions(cls, mainvar, varmaker, fetchattrs, |
237 selection, orderby, restrictions, user, |
237 selection, orderby, restrictions, user, |
238 ordermethod='fetch_order', visited=None): |
238 ordermethod='fetch_order', visited=None): |
239 eschema = cls.e_schema |
239 eschema = cls.e_schema |
306 |
306 |
307 def pre_add_hook(self): |
307 def pre_add_hook(self): |
308 """hook called by the repository before doing anything to add the entity |
308 """hook called by the repository before doing anything to add the entity |
309 (before_add entity hooks have not been called yet). This give the |
309 (before_add entity hooks have not been called yet). This give the |
310 occasion to do weird stuff such as autocast (File -> Image for instance). |
310 occasion to do weird stuff such as autocast (File -> Image for instance). |
311 |
311 |
312 This method must return the actual entity to be added. |
312 This method must return the actual entity to be added. |
313 """ |
313 """ |
314 return self |
314 return self |
315 |
315 |
316 def set_eid(self, eid): |
316 def set_eid(self, eid): |
317 self.eid = self['eid'] = eid |
317 self.eid = self['eid'] = eid |
318 |
318 |
319 def has_eid(self): |
319 def has_eid(self): |
320 """return True if the entity has an attributed eid (False |
320 """return True if the entity has an attributed eid (False |
331 has an eid attributed though it's not saved (eg during before_add_entity |
331 has an eid attributed though it's not saved (eg during before_add_entity |
332 hooks). You can use this method to ensure the entity has an eid *and* is |
332 hooks). You can use this method to ensure the entity has an eid *and* is |
333 saved in its source. |
333 saved in its source. |
334 """ |
334 """ |
335 return self.has_eid() and self._is_saved |
335 return self.has_eid() and self._is_saved |
336 |
336 |
337 @cached |
337 @cached |
338 def metainformation(self): |
338 def metainformation(self): |
339 res = dict(zip(('type', 'source', 'extid'), self.req.describe(self.eid))) |
339 res = dict(zip(('type', 'source', 'extid'), self.req.describe(self.eid))) |
340 res['source'] = self.req.source_defs()[res['source']] |
340 res['source'] = self.req.source_defs()[res['source']] |
341 return res |
341 return res |
347 def check_perm(self, action): |
347 def check_perm(self, action): |
348 self.e_schema.check_perm(self.req, action, self.eid) |
348 self.e_schema.check_perm(self.req, action, self.eid) |
349 |
349 |
350 def has_perm(self, action): |
350 def has_perm(self, action): |
351 return self.e_schema.has_perm(self.req, action, self.eid) |
351 return self.e_schema.has_perm(self.req, action, self.eid) |
352 |
352 |
353 def view(self, vid, __registry='views', **kwargs): |
353 def view(self, vid, __registry='views', **kwargs): |
354 """shortcut to apply a view on this entity""" |
354 """shortcut to apply a view on this entity""" |
355 return self.vreg.render(__registry, vid, self.req, rset=self.rset, |
355 return self.vreg.render(__registry, vid, self.req, rset=self.rset, |
356 row=self.row, col=self.col, **kwargs) |
356 row=self.row, col=self.col, **kwargs) |
357 |
357 |
450 def mtc_transform(self, data, format, target_format, encoding, |
450 def mtc_transform(self, data, format, target_format, encoding, |
451 _engine=ENGINE): |
451 _engine=ENGINE): |
452 trdata = TransformData(data, format, encoding, appobject=self) |
452 trdata = TransformData(data, format, encoding, appobject=self) |
453 data = _engine.convert(trdata, target_format).decode() |
453 data = _engine.convert(trdata, target_format).decode() |
454 if format == 'text/html': |
454 if format == 'text/html': |
455 data = soup2xhtml(data, self.req.encoding) |
455 data = soup2xhtml(data, self.req.encoding) |
456 return data |
456 return data |
457 |
457 |
458 # entity cloning ########################################################## |
458 # entity cloning ########################################################## |
459 |
459 |
460 def copy_relations(self, ceid): |
460 def copy_relations(self, ceid): |
461 """copy relations of the object with the given eid on this object |
461 """copy relations of the object with the given eid on this object |
462 |
462 |
515 def as_rset(self): |
515 def as_rset(self): |
516 """returns a resultset containing `self` information""" |
516 """returns a resultset containing `self` information""" |
517 rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s', |
517 rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s', |
518 {'x': self.eid}, [(self.id,)]) |
518 {'x': self.eid}, [(self.id,)]) |
519 return self.req.decorate_rset(rset) |
519 return self.req.decorate_rset(rset) |
520 |
520 |
521 def to_complete_relations(self): |
521 def to_complete_relations(self): |
522 """by default complete final relations to when calling .complete()""" |
522 """by default complete final relations to when calling .complete()""" |
523 for rschema in self.e_schema.subject_relations(): |
523 for rschema in self.e_schema.subject_relations(): |
524 if rschema.is_final(): |
524 if rschema.is_final(): |
525 continue |
525 continue |
531 matching_groups = self.req.user.matching_groups |
531 matching_groups = self.req.user.matching_groups |
532 if matching_groups(rschema.get_groups('read')) and \ |
532 if matching_groups(rschema.get_groups('read')) and \ |
533 all(matching_groups(es.get_groups('read')) |
533 all(matching_groups(es.get_groups('read')) |
534 for es in rschema.objects(self.e_schema)): |
534 for es in rschema.objects(self.e_schema)): |
535 yield rschema, 'subject' |
535 yield rschema, 'subject' |
536 |
536 |
537 def to_complete_attributes(self, skip_bytes=True): |
537 def to_complete_attributes(self, skip_bytes=True): |
538 for rschema, attrschema in self.e_schema.attribute_definitions(): |
538 for rschema, attrschema in self.e_schema.attribute_definitions(): |
539 # skip binary data by default |
539 # skip binary data by default |
540 if skip_bytes and attrschema.type == 'Bytes': |
540 if skip_bytes and attrschema.type == 'Bytes': |
541 continue |
541 continue |
546 if not self.req.user.matching_groups(rschema.get_groups('read')) \ |
546 if not self.req.user.matching_groups(rschema.get_groups('read')) \ |
547 or attrschema.type == 'Password': |
547 or attrschema.type == 'Password': |
548 self[attr] = None |
548 self[attr] = None |
549 continue |
549 continue |
550 yield attr |
550 yield attr |
551 |
551 |
552 def complete(self, attributes=None, skip_bytes=True): |
552 def complete(self, attributes=None, skip_bytes=True): |
553 """complete this entity by adding missing attributes (i.e. query the |
553 """complete this entity by adding missing attributes (i.e. query the |
554 repository to fill the entity) |
554 repository to fill the entity) |
555 |
555 |
556 :type skip_bytes: bool |
556 :type skip_bytes: bool |
616 rrset = ResultSet([], rql, {'x': self.eid}) |
616 rrset = ResultSet([], rql, {'x': self.eid}) |
617 self.req.decorate_rset(rrset) |
617 self.req.decorate_rset(rrset) |
618 else: |
618 else: |
619 rrset = self.req.eid_rset(value) |
619 rrset = self.req.eid_rset(value) |
620 self.set_related_cache(rtype, x, rrset) |
620 self.set_related_cache(rtype, x, rrset) |
621 |
621 |
622 def get_value(self, name): |
622 def get_value(self, name): |
623 """get value for the attribute relation <name>, query the repository |
623 """get value for the attribute relation <name>, query the repository |
624 to get the value if necessary. |
624 to get the value if necessary. |
625 |
625 |
626 :type name: str |
626 :type name: str |
652 self[name] = value = None |
652 self[name] = value = None |
653 return value |
653 return value |
654 |
654 |
655 def related(self, rtype, role='subject', limit=None, entities=False): |
655 def related(self, rtype, role='subject', limit=None, entities=False): |
656 """returns a resultset of related entities |
656 """returns a resultset of related entities |
657 |
657 |
658 :param role: is the role played by 'self' in the relation ('subject' or 'object') |
658 :param role: is the role played by 'self' in the relation ('subject' or 'object') |
659 :param limit: resultset's maximum size |
659 :param limit: resultset's maximum size |
660 :param entities: if True, the entites are returned; if False, a result set is returned |
660 :param entities: if True, the entites are returned; if False, a result set is returned |
661 """ |
661 """ |
662 try: |
662 try: |
698 rql.split(' WHERE ', 1)[1]) |
698 rql.split(' WHERE ', 1)[1]) |
699 elif not ' ORDERBY ' in rql: |
699 elif not ' ORDERBY ' in rql: |
700 args = tuple(rql.split(' WHERE ', 1)) |
700 args = tuple(rql.split(' WHERE ', 1)) |
701 rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % args |
701 rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % args |
702 return rql |
702 return rql |
703 |
703 |
704 # generic vocabulary methods ############################################## |
704 # generic vocabulary methods ############################################## |
705 |
705 |
706 @obsolete('see new form api') |
706 @obsolete('see new form api') |
707 def vocabulary(self, rtype, role='subject', limit=None): |
707 def vocabulary(self, rtype, role='subject', limit=None): |
708 """vocabulary functions must return a list of couples |
708 """vocabulary functions must return a list of couples |
709 (label, eid) that will typically be used to fill the |
709 (label, eid) that will typically be used to fill the |
710 edition view's combobox. |
710 edition view's combobox. |
711 |
711 |
712 If `eid` is None in one of these couples, it should be |
712 If `eid` is None in one of these couples, it should be |
713 interpreted as a separator in case vocabulary results are grouped |
713 interpreted as a separator in case vocabulary results are grouped |
714 """ |
714 """ |
715 from cubicweb.web.form import EntityFieldsForm |
715 from cubicweb.web.form import EntityFieldsForm |
716 from logilab.common.testlib import mock_object |
716 from logilab.common.testlib import mock_object |
717 form = EntityFieldsForm(self.req, entity=self) |
717 form = EntityFieldsForm(self.req, entity=self) |
718 field = mock_object(name=rtype, role=role) |
718 field = mock_object(name=rtype, role=role) |
719 return form.form_field_vocabulary(field, limit) |
719 return form.form_field_vocabulary(field, limit) |
720 |
720 |
721 def unrelated_rql(self, rtype, targettype, role, ordermethod=None, |
721 def unrelated_rql(self, rtype, targettype, role, ordermethod=None, |
722 vocabconstraints=True): |
722 vocabconstraints=True): |
723 """build a rql to fetch `targettype` entities unrelated to this entity |
723 """build a rql to fetch `targettype` entities unrelated to this entity |
724 using (rtype, role) relation |
724 using (rtype, role) relation |
725 """ |
725 """ |
751 # ensure we have an order defined |
751 # ensure we have an order defined |
752 if not ' ORDERBY ' in rql: |
752 if not ' ORDERBY ' in rql: |
753 before, after = rql.split(' WHERE ', 1) |
753 before, after = rql.split(' WHERE ', 1) |
754 rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after) |
754 rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after) |
755 return rql |
755 return rql |
756 |
756 |
757 def unrelated(self, rtype, targettype, role='subject', limit=None, |
757 def unrelated(self, rtype, targettype, role='subject', limit=None, |
758 ordermethod=None): |
758 ordermethod=None): |
759 """return a result set of target type objects that may be related |
759 """return a result set of target type objects that may be related |
760 by a given relation, with self as subject or object |
760 by a given relation, with self as subject or object |
761 """ |
761 """ |
764 before, after = rql.split(' WHERE ', 1) |
764 before, after = rql.split(' WHERE ', 1) |
765 rql = '%s LIMIT %s WHERE %s' % (before, limit, after) |
765 rql = '%s LIMIT %s WHERE %s' % (before, limit, after) |
766 if self.has_eid(): |
766 if self.has_eid(): |
767 return self.req.execute(rql, {'x': self.eid}) |
767 return self.req.execute(rql, {'x': self.eid}) |
768 return self.req.execute(rql) |
768 return self.req.execute(rql) |
769 |
769 |
770 # relations cache handling ################################################ |
770 # relations cache handling ################################################ |
771 |
771 |
772 def relation_cached(self, rtype, role): |
772 def relation_cached(self, rtype, role): |
773 """return true if the given relation is already cached on the instance |
773 """return true if the given relation is already cached on the instance |
774 """ |
774 """ |
775 return '%s_%s' % (rtype, role) in self._related_cache |
775 return '%s_%s' % (rtype, role) in self._related_cache |
776 |
776 |
777 def related_cache(self, rtype, role, entities=True, limit=None): |
777 def related_cache(self, rtype, role, entities=True, limit=None): |
778 """return values for the given relation if it's cached on the instance, |
778 """return values for the given relation if it's cached on the instance, |
779 else raise `KeyError` |
779 else raise `KeyError` |
780 """ |
780 """ |
781 res = self._related_cache['%s_%s' % (rtype, role)][entities] |
781 res = self._related_cache['%s_%s' % (rtype, role)][entities] |
783 if entities: |
783 if entities: |
784 res = res[:limit] |
784 res = res[:limit] |
785 else: |
785 else: |
786 res = res.limit(limit) |
786 res = res.limit(limit) |
787 return res |
787 return res |
788 |
788 |
789 def set_related_cache(self, rtype, role, rset, col=0): |
789 def set_related_cache(self, rtype, role, rset, col=0): |
790 """set cached values for the given relation""" |
790 """set cached values for the given relation""" |
791 if rset: |
791 if rset: |
792 related = list(rset.entities(col)) |
792 related = list(rset.entities(col)) |
793 rschema = self.schema.rschema(rtype) |
793 rschema = self.schema.rschema(rtype) |
803 for rentity in related: |
803 for rentity in related: |
804 rentity._related_cache['%s_%s' % (rtype, target)] = (self.as_rset(), [self]) |
804 rentity._related_cache['%s_%s' % (rtype, target)] = (self.as_rset(), [self]) |
805 else: |
805 else: |
806 related = [] |
806 related = [] |
807 self._related_cache['%s_%s' % (rtype, role)] = (rset, related) |
807 self._related_cache['%s_%s' % (rtype, role)] = (rset, related) |
808 |
808 |
809 def clear_related_cache(self, rtype=None, role=None): |
809 def clear_related_cache(self, rtype=None, role=None): |
810 """clear cached values for the given relation or the entire cache if |
810 """clear cached values for the given relation or the entire cache if |
811 no relation is given |
811 no relation is given |
812 """ |
812 """ |
813 if rtype is None: |
813 if rtype is None: |
814 self._related_cache = {} |
814 self._related_cache = {} |
815 else: |
815 else: |
816 assert role |
816 assert role |
817 self._related_cache.pop('%s_%s' % (rtype, role), None) |
817 self._related_cache.pop('%s_%s' % (rtype, role), None) |
818 |
818 |
819 # raw edition utilities ################################################### |
819 # raw edition utilities ################################################### |
820 |
820 |
821 def set_attributes(self, **kwargs): |
821 def set_attributes(self, **kwargs): |
822 assert kwargs |
822 assert kwargs |
823 relations = [] |
823 relations = [] |
824 for key in kwargs: |
824 for key in kwargs: |
825 relations.append('X %s %%(%s)s' % (key, key)) |
825 relations.append('X %s %%(%s)s' % (key, key)) |
827 self.update(kwargs) |
827 self.update(kwargs) |
828 # and now update the database |
828 # and now update the database |
829 kwargs['x'] = self.eid |
829 kwargs['x'] = self.eid |
830 self.req.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), |
830 self.req.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), |
831 kwargs, 'x') |
831 kwargs, 'x') |
832 |
832 |
833 def delete(self): |
833 def delete(self): |
834 assert self.has_eid(), self.eid |
834 assert self.has_eid(), self.eid |
835 self.req.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema, |
835 self.req.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema, |
836 {'x': self.eid}) |
836 {'x': self.eid}) |
837 |
837 |
838 # server side utilities ################################################### |
838 # server side utilities ################################################### |
839 |
839 |
840 def set_defaults(self): |
840 def set_defaults(self): |
841 """set default values according to the schema""" |
841 """set default values according to the schema""" |
842 self._default_set = set() |
842 self._default_set = set() |
843 for attr, value in self.e_schema.defaults(): |
843 for attr, value in self.e_schema.defaults(): |
844 if not self.has_key(attr): |
844 if not self.has_key(attr): |
900 self.exception("can't add value of %s to text index for entity %s", |
900 self.exception("can't add value of %s to text index for entity %s", |
901 rschema, self.eid) |
901 rschema, self.eid) |
902 continue |
902 continue |
903 if value: |
903 if value: |
904 words += tokenize(value) |
904 words += tokenize(value) |
905 |
905 |
906 for rschema, role in self.e_schema.fulltext_relations(): |
906 for rschema, role in self.e_schema.fulltext_relations(): |
907 if role == 'subject': |
907 if role == 'subject': |
908 for entity in getattr(self, rschema.type): |
908 for entity in getattr(self, rschema.type): |
909 words += entity.get_words() |
909 words += entity.get_words() |
910 else: # if role == 'object': |
910 else: # if role == 'object': |
945 def __get__(self, eobj, eclass): |
945 def __get__(self, eobj, eclass): |
946 if eobj is None: |
946 if eobj is None: |
947 raise AttributeError('%s cannot be only be accessed from instances' |
947 raise AttributeError('%s cannot be only be accessed from instances' |
948 % self._rtype) |
948 % self._rtype) |
949 return eobj.related(self._rtype, self._role, entities=True) |
949 return eobj.related(self._rtype, self._role, entities=True) |
950 |
950 |
951 def __set__(self, eobj, value): |
951 def __set__(self, eobj, value): |
952 raise NotImplementedError |
952 raise NotImplementedError |
953 |
953 |
954 |
954 |
955 class SubjectRelation(Relation): |
955 class SubjectRelation(Relation): |
956 """descriptor that controls schema relation access""" |
956 """descriptor that controls schema relation access""" |
957 _role = 'subject' |
957 _role = 'subject' |
958 |
958 |
959 class ObjectRelation(Relation): |
959 class ObjectRelation(Relation): |
960 """descriptor that controls schema relation access""" |
960 """descriptor that controls schema relation access""" |
961 _role = 'object' |
961 _role = 'object' |
962 |
962 |
963 from logging import getLogger |
963 from logging import getLogger |