15 from cubicweb import ValidationError |
15 from cubicweb import ValidationError |
16 from cubicweb.schema import RQLConstraint, RQLUniqueConstraint |
16 from cubicweb.schema import RQLConstraint, RQLUniqueConstraint |
17 from cubicweb.selectors import implements |
17 from cubicweb.selectors import implements |
18 from cubicweb.uilib import soup2xhtml |
18 from cubicweb.uilib import soup2xhtml |
19 from cubicweb.server import hook |
19 from cubicweb.server import hook |
|
20 from cubicweb.server.hook import set_operation |
20 |
21 |
21 # special relations that don't have to be checked for integrity, usually |
22 # special relations that don't have to be checked for integrity, usually |
22 # because they are handled internally by hooks (so we trust ourselves) |
23 # because they are handled internally by hooks (so we trust ourselves) |
23 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by', |
24 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by', |
24 'is', 'is_instance_of', |
25 'is', 'is_instance_of', |
60 |
61 |
61 class _CheckRequiredRelationOperation(hook.LateOperation): |
62 class _CheckRequiredRelationOperation(hook.LateOperation): |
62 """checking relation cardinality has to be done after commit in |
63 """checking relation cardinality has to be done after commit in |
63 case the relation is being replaced |
64 case the relation is being replaced |
64 """ |
65 """ |
65 eid, rtype = None, None |
66 role = key = base_rql = None |
66 |
67 |
67 def precommit_event(self): |
68 def precommit_event(self): |
68 # recheck pending eids |
69 session =self.session |
69 if self.session.deleted_in_transaction(self.eid): |
70 pendingeids = session.transaction_data.get('pendingeids', ()) |
70 return |
71 pendingrtypes = session.transaction_data.get('pendingrtypes', ()) |
71 if self.rtype in self.session.transaction_data.get('pendingrtypes', ()): |
72 # poping key is not optional: if further operation trigger new deletion |
72 return |
73 # of relation, we'll need a new operation |
73 if self.session.execute(*self._rql()).rowcount < 1: |
74 for eid, rtype in session.transaction_data.pop(self.key): |
74 etype = self.session.describe(self.eid)[0] |
75 # recheck pending eids / relation types |
75 _ = self.session._ |
76 if eid in pendingeids: |
76 msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)') |
77 continue |
77 msg %= {'rtype': _(self.rtype), 'etype': _(etype), 'eid': self.eid} |
78 if rtype in pendingrtypes: |
78 qname = role_name(self.rtype, self.role) |
79 continue |
79 raise ValidationError(self.eid, {qname: msg}) |
80 if not session.execute(self.base_rql % rtype, {'x': eid}, 'x'): |
80 |
81 etype = session.describe(eid)[0] |
81 def commit_event(self): |
82 _ = session._ |
82 pass |
83 msg = _('at least one relation %(rtype)s is required on ' |
83 |
84 '%(etype)s (%(eid)s)') |
84 def _rql(self): |
85 msg %= {'rtype': _(rtype), 'etype': _(etype), 'eid': eid} |
85 raise NotImplementedError() |
86 raise ValidationError(eid, {role_name(rtype, self.role): msg}) |
86 |
87 |
87 |
88 |
88 class _CheckSRelationOp(_CheckRequiredRelationOperation): |
89 class _CheckSRelationOp(_CheckRequiredRelationOperation): |
89 """check required subject relation""" |
90 """check required subject relation""" |
90 role = 'subject' |
91 role = 'subject' |
91 def _rql(self): |
92 key = '_cwisrel' |
92 return 'Any O WHERE S eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x' |
93 base_rql = 'Any O WHERE S eid %%(x)s, S %s O' |
93 |
|
94 |
94 |
95 class _CheckORelationOp(_CheckRequiredRelationOperation): |
95 class _CheckORelationOp(_CheckRequiredRelationOperation): |
96 """check required object relation""" |
96 """check required object relation""" |
97 role = 'object' |
97 role = 'object' |
98 def _rql(self): |
98 key = '_cwiorel' |
99 return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x' |
99 base_rql = 'Any S WHERE O eid %%(x)s, S %s O' |
100 |
100 |
101 |
101 |
102 class IntegrityHook(hook.Hook): |
102 class IntegrityHook(hook.Hook): |
103 __abstract__ = True |
103 __abstract__ = True |
104 category = 'integrity' |
104 category = 'integrity' |
109 __regid__ = 'checkcard' |
109 __regid__ = 'checkcard' |
110 events = ('after_add_entity', 'before_delete_relation') |
110 events = ('after_add_entity', 'before_delete_relation') |
111 |
111 |
112 def __call__(self): |
112 def __call__(self): |
113 getattr(self, self.event)() |
113 getattr(self, self.event)() |
114 |
|
115 def checkrel_if_necessary(self, opcls, rtype, eid): |
|
116 """check an equivalent operation has not already been added""" |
|
117 for op in self._cw.pending_operations: |
|
118 if isinstance(op, opcls) and op.rtype == rtype and op.eid == eid: |
|
119 break |
|
120 else: |
|
121 opcls(self._cw, rtype=rtype, eid=eid) |
|
122 |
114 |
123 def after_add_entity(self): |
115 def after_add_entity(self): |
124 eid = self.entity.eid |
116 eid = self.entity.eid |
125 eschema = self.entity.e_schema |
117 eschema = self.entity.e_schema |
126 for rschema, targetschemas, role in eschema.relation_definitions(): |
118 for rschema, targetschemas, role in eschema.relation_definitions(): |
127 # skip automatically handled relations |
119 # skip automatically handled relations |
128 if rschema.type in DONT_CHECK_RTYPES_ON_ADD: |
120 if rschema.type in DONT_CHECK_RTYPES_ON_ADD: |
129 continue |
121 continue |
130 opcls = role == 'subject' and _CheckSRelationOp or _CheckORelationOp |
|
131 rdef = rschema.role_rdef(eschema, targetschemas[0], role) |
122 rdef = rschema.role_rdef(eschema, targetschemas[0], role) |
132 if rdef.role_cardinality(role) in '1+': |
123 if rdef.role_cardinality(role) in '1+': |
133 self.checkrel_if_necessary(opcls, rschema.type, eid) |
124 if role == 'subject': |
|
125 set_operation(self._cw, '_cwisrel', (eid, rschema.type), |
|
126 _CheckSRelationOp) |
|
127 else: |
|
128 set_operation(self._cw, '_cwiorel', (eid, rschema.type), |
|
129 _CheckORelationOp) |
134 |
130 |
135 def before_delete_relation(self): |
131 def before_delete_relation(self): |
136 rtype = self.rtype |
132 rtype = self.rtype |
137 if rtype in DONT_CHECK_RTYPES_ON_DEL: |
133 if rtype in DONT_CHECK_RTYPES_ON_DEL: |
138 return |
134 return |
139 session = self._cw |
135 session = self._cw |
140 eidfrom, eidto = self.eidfrom, self.eidto |
136 eidfrom, eidto = self.eidfrom, self.eidto |
141 card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality') |
|
142 pendingrdefs = session.transaction_data.get('pendingrdefs', ()) |
137 pendingrdefs = session.transaction_data.get('pendingrdefs', ()) |
143 if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs: |
138 if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs: |
144 return |
139 return |
|
140 card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality') |
145 if card[0] in '1+' and not session.deleted_in_transaction(eidfrom): |
141 if card[0] in '1+' and not session.deleted_in_transaction(eidfrom): |
146 self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom) |
142 set_operation(self._cw, '_cwisrel', (eidfrom, rtype), |
|
143 _CheckSRelationOp) |
147 if card[1] in '1+' and not session.deleted_in_transaction(eidto): |
144 if card[1] in '1+' and not session.deleted_in_transaction(eidto): |
148 self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto) |
145 set_operation(self._cw, '_cwiorel', (eidto, rtype), |
|
146 _CheckORelationOp) |
149 |
147 |
150 |
148 |
151 class _CheckConstraintsOp(hook.LateOperation): |
149 class _CheckConstraintsOp(hook.LateOperation): |
152 """check a new relation satisfy its constraints |
150 """check a new relation satisfy its constraints |
153 """ |
151 """ |
289 |
287 |
290 # 'active' integrity hooks: you usually don't want to deactivate them, they are |
288 # 'active' integrity hooks: you usually don't want to deactivate them, they are |
291 # not really integrity check, they maintain consistency on changes |
289 # not really integrity check, they maintain consistency on changes |
292 |
290 |
293 class _DelayedDeleteOp(hook.Operation): |
291 class _DelayedDeleteOp(hook.Operation): |
294 """delete the object of composite relation except if the relation |
292 """delete the object of composite relation except if the relation has |
295 has actually been redirected to another composite |
293 actually been redirected to another composite |
296 """ |
294 """ |
|
295 key = base_rql = None |
297 |
296 |
298 def precommit_event(self): |
297 def precommit_event(self): |
299 session = self.session |
298 session = self.session |
300 # don't do anything if the entity is being created or deleted |
299 pendingeids = session.transaction_data.get('pendingeids', ()) |
301 if not (session.deleted_in_transaction(self.eid) or |
300 neweids = session.transaction_data.get('neweids', ()) |
302 session.added_in_transaction(self.eid)): |
301 # poping key is not optional: if further operation trigger new deletion |
303 etype = session.describe(self.eid)[0] |
302 # of composite relation, we'll need a new operation |
304 session.execute('DELETE %s X WHERE X eid %%(x)s, NOT %s' |
303 for eid, rtype in session.transaction_data.pop(self.key): |
305 % (etype, self.relation), |
304 # don't do anything if the entity is being created or deleted |
306 {'x': self.eid}, 'x') |
305 if not (eid in pendingeids or eid in neweids): |
|
306 etype = session.describe(eid)[0] |
|
307 session.execute(self.base_rql % (etype, rtype), {'x': eid}, 'x') |
|
308 |
|
309 class _DelayedDeleteSEntityOp(_DelayedDeleteOp): |
|
310 """delete orphan subject entity of a composite relation""" |
|
311 key = '_cwiscomp' |
|
312 base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT X %s Y' |
|
313 |
|
314 class _DelayedDeleteOEntityOp(_DelayedDeleteOp): |
|
315 """check required object relation""" |
|
316 key = '_cwiocomp' |
|
317 base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT Y %s X' |
307 |
318 |
308 |
319 |
309 class DeleteCompositeOrphanHook(hook.Hook): |
320 class DeleteCompositeOrphanHook(hook.Hook): |
310 """delete the composed of a composite relation when this relation is deleted |
321 """delete the composed of a composite relation when this relation is deleted |
311 """ |
322 """ |
321 self._cw.describe(self.eidto)[0]) in pendingrdefs: |
332 self._cw.describe(self.eidto)[0]) in pendingrdefs: |
322 return |
333 return |
323 composite = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto, |
334 composite = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto, |
324 'composite') |
335 'composite') |
325 if composite == 'subject': |
336 if composite == 'subject': |
326 _DelayedDeleteOp(self._cw, eid=self.eidto, |
337 set_operation(self._cw, '_cwiocomp', (self.eidto, self.rtype), |
327 relation='Y %s X' % self.rtype) |
338 _DelayedDeleteOEntityOp) |
328 elif composite == 'object': |
339 elif composite == 'object': |
329 _DelayedDeleteOp(self._cw, eid=self.eidfrom, |
340 set_operation(self._cw, '_cwiscomp', (self.eidfrom, self.rtype), |
330 relation='X %s Y' % self.rtype) |
341 _DelayedDeleteSEntityOp) |