60 if 'uniquecstrholder' in session.transaction_data: |
59 if 'uniquecstrholder' in session.transaction_data: |
61 del session.transaction_data['uniquecstrholder'] |
60 del session.transaction_data['uniquecstrholder'] |
62 _UNIQUE_CONSTRAINTS_LOCK.release() |
61 _UNIQUE_CONSTRAINTS_LOCK.release() |
63 |
62 |
64 class _ReleaseUniqueConstraintsOperation(hook.Operation): |
63 class _ReleaseUniqueConstraintsOperation(hook.Operation): |
65 def commit_event(self): |
|
66 pass |
|
67 def postcommit_event(self): |
64 def postcommit_event(self): |
68 _release_unique_cstr_lock(self.session) |
65 _release_unique_cstr_lock(self.session) |
69 def rollback_event(self): |
66 def rollback_event(self): |
70 _release_unique_cstr_lock(self.session) |
67 _release_unique_cstr_lock(self.session) |
71 |
68 |
72 |
69 |
73 class _CheckRequiredRelationOperation(hook.LateOperation): |
70 class _CheckRequiredRelationOperation(hook.DataOperationMixIn, |
74 """checking relation cardinality has to be done after commit in |
71 hook.LateOperation): |
75 case the relation is being replaced |
72 """checking relation cardinality has to be done after commit in case the |
76 """ |
73 relation is being replaced |
|
74 """ |
|
75 containercls = list |
77 role = key = base_rql = None |
76 role = key = base_rql = None |
78 |
77 |
79 def precommit_event(self): |
78 def precommit_event(self): |
80 session =self.session |
79 session = self.session |
81 pendingeids = session.transaction_data.get('pendingeids', ()) |
80 pendingeids = session.transaction_data.get('pendingeids', ()) |
82 pendingrtypes = session.transaction_data.get('pendingrtypes', ()) |
81 pendingrtypes = session.transaction_data.get('pendingrtypes', ()) |
83 # poping key is not optional: if further operation trigger new deletion |
82 for eid, rtype in self.get_data(): |
84 # of relation, we'll need a new operation |
|
85 for eid, rtype in session.transaction_data.pop(self.key): |
|
86 # recheck pending eids / relation types |
83 # recheck pending eids / relation types |
87 if eid in pendingeids: |
84 if eid in pendingeids: |
88 continue |
85 continue |
89 if rtype in pendingrtypes: |
86 if rtype in pendingrtypes: |
90 continue |
87 continue |
98 |
95 |
99 |
96 |
100 class _CheckSRelationOp(_CheckRequiredRelationOperation): |
97 class _CheckSRelationOp(_CheckRequiredRelationOperation): |
101 """check required subject relation""" |
98 """check required subject relation""" |
102 role = 'subject' |
99 role = 'subject' |
103 key = '_cwisrel' |
|
104 base_rql = 'Any O WHERE S eid %%(x)s, S %s O' |
100 base_rql = 'Any O WHERE S eid %%(x)s, S %s O' |
105 |
101 |
106 class _CheckORelationOp(_CheckRequiredRelationOperation): |
102 class _CheckORelationOp(_CheckRequiredRelationOperation): |
107 """check required object relation""" |
103 """check required object relation""" |
108 role = 'object' |
104 role = 'object' |
109 key = '_cwiorel' |
|
110 base_rql = 'Any S WHERE O eid %%(x)s, S %s O' |
105 base_rql = 'Any S WHERE O eid %%(x)s, S %s O' |
111 |
106 |
112 |
107 |
113 class IntegrityHook(hook.Hook): |
108 class IntegrityHook(hook.Hook): |
114 __abstract__ = True |
109 __abstract__ = True |
115 category = 'integrity' |
110 category = 'integrity' |
116 |
111 |
117 |
112 |
118 class CheckCardinalityHook(IntegrityHook): |
113 class CheckCardinalityHookBeforeDeleteRelation(IntegrityHook): |
119 """check cardinalities are satisfied""" |
114 """check cardinalities are satisfied""" |
120 __regid__ = 'checkcard' |
115 __regid__ = 'checkcard_before_delete_relation' |
121 events = ('after_add_entity', 'before_delete_relation') |
116 events = ('before_delete_relation',) |
122 |
117 |
123 def __call__(self): |
118 def __call__(self): |
124 getattr(self, self.event)() |
119 rtype = self.rtype |
125 |
120 if rtype in DONT_CHECK_RTYPES_ON_DEL: |
126 def after_add_entity(self): |
121 return |
|
122 session = self._cw |
|
123 eidfrom, eidto = self.eidfrom, self.eidto |
|
124 pendingrdefs = session.transaction_data.get('pendingrdefs', ()) |
|
125 if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs: |
|
126 return |
|
127 card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality') |
|
128 if card[0] in '1+' and not session.deleted_in_transaction(eidfrom): |
|
129 _CheckSRelationOp.get_instance(self._cw).add_data((eidfrom, rtype)) |
|
130 if card[1] in '1+' and not session.deleted_in_transaction(eidto): |
|
131 _CheckORelationOp.get_instance(self._cw).add_data((eidto, rtype)) |
|
132 |
|
133 class CheckCardinalityHookAfterAddEntity(IntegrityHook): |
|
134 """check cardinalities are satisfied""" |
|
135 __regid__ = 'checkcard_after_add_entity' |
|
136 events = ('after_add_entity',) |
|
137 |
|
138 def __call__(self): |
127 eid = self.entity.eid |
139 eid = self.entity.eid |
128 eschema = self.entity.e_schema |
140 eschema = self.entity.e_schema |
129 for rschema, targetschemas, role in eschema.relation_definitions(): |
141 for rschema, targetschemas, role in eschema.relation_definitions(): |
130 # skip automatically handled relations |
142 # skip automatically handled relations |
131 if rschema.type in DONT_CHECK_RTYPES_ON_ADD: |
143 if rschema.type in DONT_CHECK_RTYPES_ON_ADD: |
132 continue |
144 continue |
133 rdef = rschema.role_rdef(eschema, targetschemas[0], role) |
145 rdef = rschema.role_rdef(eschema, targetschemas[0], role) |
134 if rdef.role_cardinality(role) in '1+': |
146 if rdef.role_cardinality(role) in '1+': |
135 if role == 'subject': |
147 if role == 'subject': |
136 set_operation(self._cw, '_cwisrel', (eid, rschema.type), |
148 op = _CheckSRelationOp.get_instance(self._cw) |
137 _CheckSRelationOp, list) |
|
138 else: |
149 else: |
139 set_operation(self._cw, '_cwiorel', (eid, rschema.type), |
150 op = _CheckORelationOp.get_instance(self._cw) |
140 _CheckORelationOp, list) |
151 op.add_data((eid, rschema.type)) |
141 |
152 |
142 def before_delete_relation(self): |
153 def before_delete_relation(self): |
143 rtype = self.rtype |
154 rtype = self.rtype |
144 if rtype in DONT_CHECK_RTYPES_ON_DEL: |
155 if rtype in DONT_CHECK_RTYPES_ON_DEL: |
145 return |
156 return |
148 pendingrdefs = session.transaction_data.get('pendingrdefs', ()) |
159 pendingrdefs = session.transaction_data.get('pendingrdefs', ()) |
149 if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs: |
160 if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs: |
150 return |
161 return |
151 card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality') |
162 card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality') |
152 if card[0] in '1+' and not session.deleted_in_transaction(eidfrom): |
163 if card[0] in '1+' and not session.deleted_in_transaction(eidfrom): |
153 set_operation(self._cw, '_cwisrel', (eidfrom, rtype), |
164 _CheckSRelationOp.get_instance(self._cw).add_data((eidfrom, rtype)) |
154 _CheckSRelationOp, list) |
|
155 if card[1] in '1+' and not session.deleted_in_transaction(eidto): |
165 if card[1] in '1+' and not session.deleted_in_transaction(eidto): |
156 set_operation(self._cw, '_cwiorel', (eidto, rtype), |
166 _CheckORelationOp.get_instance(self._cw).add_data((eidto, rtype)) |
157 _CheckORelationOp, list) |
167 |
158 |
168 |
159 |
169 class _CheckConstraintsOp(hook.DataOperationMixIn, hook.LateOperation): |
160 class _CheckConstraintsOp(hook.LateOperation): |
|
161 """ check a new relation satisfy its constraints """ |
170 """ check a new relation satisfy its constraints """ |
162 |
171 containercls = list |
163 def precommit_event(self): |
172 def precommit_event(self): |
164 session = self.session |
173 session = self.session |
165 for values in session.transaction_data.pop('check_constraints_op'): |
174 for values in self.get_data(): |
166 eidfrom, rtype, eidto, constraints = values |
175 eidfrom, rtype, eidto, constraints = values |
167 # first check related entities have not been deleted in the same |
176 # first check related entities have not been deleted in the same |
168 # transaction |
177 # transaction |
169 if session.deleted_in_transaction(eidfrom): |
178 if session.deleted_in_transaction(eidfrom): |
170 return |
179 continue |
171 if session.deleted_in_transaction(eidto): |
180 if session.deleted_in_transaction(eidto): |
172 return |
181 continue |
173 for constraint in constraints: |
182 for constraint in constraints: |
174 # XXX |
183 # XXX |
175 # * lock RQLConstraint as well? |
184 # * lock RQLConstraint as well? |
176 # * use a constraint id to use per constraint lock and avoid |
185 # * use a constraint id to use per constraint lock and avoid |
177 # unnecessary commit serialization ? |
186 # unnecessary commit serialization ? |
215 __regid__ = 'checkattrconstraint' |
220 __regid__ = 'checkattrconstraint' |
216 events = ('after_add_entity', 'after_update_entity') |
221 events = ('after_add_entity', 'after_update_entity') |
217 |
222 |
218 def __call__(self): |
223 def __call__(self): |
219 eschema = self.entity.e_schema |
224 eschema = self.entity.e_schema |
220 for attr in self.entity.edited_attributes: |
225 for attr in self.entity.cw_edited: |
221 if eschema.subjrels[attr].final: |
226 if eschema.subjrels[attr].final: |
222 constraints = [c for c in eschema.rdef(attr).constraints |
227 constraints = [c for c in eschema.rdef(attr).constraints |
223 if isinstance(c, (RQLUniqueConstraint, RQLConstraint))] |
228 if isinstance(c, (RQLUniqueConstraint, RQLConstraint))] |
224 if constraints: |
229 if constraints: |
225 hook.set_operation(self._cw, 'check_constraints_op', |
230 _CheckConstraintsOp.get_instance(self._cw).add_data( |
226 (self.entity.eid, attr, None, tuple(constraints)), |
231 (self.entity.eid, attr, None, constraints)) |
227 _CheckConstraintsOp, list) |
|
228 |
232 |
229 |
233 |
230 class CheckUniqueHook(IntegrityHook): |
234 class CheckUniqueHook(IntegrityHook): |
231 __regid__ = 'checkunique' |
235 __regid__ = 'checkunique' |
232 events = ('before_add_entity', 'before_update_entity') |
236 events = ('before_add_entity', 'before_update_entity') |
233 |
237 |
234 def __call__(self): |
238 def __call__(self): |
235 entity = self.entity |
239 entity = self.entity |
236 eschema = entity.e_schema |
240 eschema = entity.e_schema |
237 for attr in entity.edited_attributes: |
241 for attr, val in entity.cw_edited.iteritems(): |
238 if eschema.subjrels[attr].final and eschema.has_unique_values(attr): |
242 if eschema.subjrels[attr].final and eschema.has_unique_values(attr): |
239 val = entity[attr] |
|
240 if val is None: |
243 if val is None: |
241 continue |
244 continue |
242 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr) |
245 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr) |
243 rset = self._cw.execute(rql, {'val': val}) |
246 rset = self._cw.execute(rql, {'val': val}) |
244 if rset and rset[0][0] != entity.eid: |
247 if rset and rset[0][0] != entity.eid: |
253 __regid__ = 'checkownersgroup' |
256 __regid__ = 'checkownersgroup' |
254 __select__ = IntegrityHook.__select__ & is_instance('CWGroup') |
257 __select__ = IntegrityHook.__select__ & is_instance('CWGroup') |
255 events = ('before_delete_entity', 'before_update_entity') |
258 events = ('before_delete_entity', 'before_update_entity') |
256 |
259 |
257 def __call__(self): |
260 def __call__(self): |
258 if self.event == 'before_delete_entity' and self.entity.name == 'owners': |
261 entity = self.entity |
|
262 if self.event == 'before_delete_entity' and entity.name == 'owners': |
259 msg = self._cw._('can\'t be deleted') |
263 msg = self._cw._('can\'t be deleted') |
260 raise ValidationError(self.entity.eid, {None: msg}) |
264 raise ValidationError(entity.eid, {None: msg}) |
261 elif self.event == 'before_update_entity' and \ |
265 elif self.event == 'before_update_entity' \ |
262 'name' in self.entity.edited_attributes: |
266 and 'name' in entity.cw_edited: |
263 newname = self.entity.pop('name') |
267 oldname, newname = entity.cw_edited.oldnewvalue('name') |
264 oldname = self.entity.name |
|
265 if oldname == 'owners' and newname != oldname: |
268 if oldname == 'owners' and newname != oldname: |
266 qname = role_name('name', 'subject') |
269 qname = role_name('name', 'subject') |
267 msg = self._cw._('can\'t be changed') |
270 msg = self._cw._('can\'t be changed') |
268 raise ValidationError(self.entity.eid, {qname: msg}) |
271 raise ValidationError(entity.eid, {qname: msg}) |
269 self.entity['name'] = newname |
|
270 |
272 |
271 |
273 |
272 class TidyHtmlFields(IntegrityHook): |
274 class TidyHtmlFields(IntegrityHook): |
273 """tidy HTML in rich text strings""" |
275 """tidy HTML in rich text strings""" |
274 __regid__ = 'htmltidy' |
276 __regid__ = 'htmltidy' |
275 events = ('before_add_entity', 'before_update_entity') |
277 events = ('before_add_entity', 'before_update_entity') |
276 |
278 |
277 def __call__(self): |
279 def __call__(self): |
278 entity = self.entity |
280 entity = self.entity |
279 metaattrs = entity.e_schema.meta_attributes() |
281 metaattrs = entity.e_schema.meta_attributes() |
|
282 edited = entity.cw_edited |
280 for metaattr, (metadata, attr) in metaattrs.iteritems(): |
283 for metaattr, (metadata, attr) in metaattrs.iteritems(): |
281 if metadata == 'format' and attr in entity.edited_attributes: |
284 if metadata == 'format' and attr in edited: |
282 try: |
285 try: |
283 value = entity[attr] |
286 value = edited[attr] |
284 except KeyError: |
287 except KeyError: |
285 continue # no text to tidy |
288 continue # no text to tidy |
286 if isinstance(value, unicode): # filter out None and Binary |
289 if isinstance(value, unicode): # filter out None and Binary |
287 if getattr(entity, str(metaattr)) == 'text/html': |
290 if getattr(entity, str(metaattr)) == 'text/html': |
288 entity[attr] = soup2xhtml(value, self._cw.encoding) |
291 edited[attr] = soup2xhtml(value, self._cw.encoding) |
289 |
292 |
290 |
293 |
291 class StripCWUserLoginHook(IntegrityHook): |
294 class StripCWUserLoginHook(IntegrityHook): |
292 """ensure user logins are stripped""" |
295 """ensure user logins are stripped""" |
293 __regid__ = 'stripuserlogin' |
296 __regid__ = 'stripuserlogin' |
294 __select__ = IntegrityHook.__select__ & is_instance('CWUser') |
297 __select__ = IntegrityHook.__select__ & is_instance('CWUser') |
295 events = ('before_add_entity', 'before_update_entity',) |
298 events = ('before_add_entity', 'before_update_entity',) |
296 |
299 |
297 def __call__(self): |
300 def __call__(self): |
298 user = self.entity |
301 login = self.entity.cw_edited.get('login') |
299 if 'login' in user.edited_attributes and user.login: |
302 if login: |
300 user.login = user.login.strip() |
303 self.entity.cw_edited['login'] = login.strip() |
301 |
304 |
302 |
305 |
303 # 'active' integrity hooks: you usually don't want to deactivate them, they are |
306 # 'active' integrity hooks: you usually don't want to deactivate them, they are |
304 # not really integrity check, they maintain consistency on changes |
307 # not really integrity check, they maintain consistency on changes |
305 |
308 |
306 class _DelayedDeleteOp(hook.Operation): |
309 class _DelayedDeleteOp(hook.DataOperationMixIn, hook.Operation): |
307 """delete the object of composite relation except if the relation has |
310 """delete the object of composite relation except if the relation has |
308 actually been redirected to another composite |
311 actually been redirected to another composite |
309 """ |
312 """ |
310 key = base_rql = None |
313 base_rql = None |
311 |
314 |
312 def precommit_event(self): |
315 def precommit_event(self): |
313 session = self.session |
316 session = self.session |
314 pendingeids = session.transaction_data.get('pendingeids', ()) |
317 pendingeids = session.transaction_data.get('pendingeids', ()) |
315 neweids = session.transaction_data.get('neweids', ()) |
318 neweids = session.transaction_data.get('neweids', ()) |
316 # poping key is not optional: if further operation trigger new deletion |
319 eids_by_etype_rtype = {} |
317 # of composite relation, we'll need a new operation |
320 for eid, rtype in self.get_data(): |
318 for eid, rtype in session.transaction_data.pop(self.key): |
|
319 # don't do anything if the entity is being created or deleted |
321 # don't do anything if the entity is being created or deleted |
320 if not (eid in pendingeids or eid in neweids): |
322 if not (eid in pendingeids or eid in neweids): |
321 etype = session.describe(eid)[0] |
323 etype = session.describe(eid)[0] |
322 session.execute(self.base_rql % (etype, rtype), {'x': eid}) |
324 key = (etype, rtype) |
|
325 if key not in eids_by_etype_rtype: |
|
326 eids_by_etype_rtype[key] = [str(eid)] |
|
327 else: |
|
328 eids_by_etype_rtype[key].append(str(eid)) |
|
329 for (etype, rtype), eids in eids_by_etype_rtype.iteritems(): |
|
330 # quite unexpectedly, not deleting too many entities at a time in |
|
331 # this operation benefits to the exec speed (possibly on the RQL |
|
332 # parsing side) |
|
333 start = 0 |
|
334 incr = 500 |
|
335 while start < len(eids): |
|
336 session.execute(self.base_rql % (etype, ','.join(eids[start:start+incr]), rtype)) |
|
337 start += incr |
323 |
338 |
324 class _DelayedDeleteSEntityOp(_DelayedDeleteOp): |
339 class _DelayedDeleteSEntityOp(_DelayedDeleteOp): |
325 """delete orphan subject entity of a composite relation""" |
340 """delete orphan subject entity of a composite relation""" |
326 key = '_cwiscomp' |
341 base_rql = 'DELETE %s X WHERE X eid IN (%s), NOT X %s Y' |
327 base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT X %s Y' |
|
328 |
342 |
329 class _DelayedDeleteOEntityOp(_DelayedDeleteOp): |
343 class _DelayedDeleteOEntityOp(_DelayedDeleteOp): |
330 """check required object relation""" |
344 """check required object relation""" |
331 key = '_cwiocomp' |
345 base_rql = 'DELETE %s X WHERE X eid IN (%s), NOT Y %s X' |
332 base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT Y %s X' |
|
333 |
346 |
334 |
347 |
335 class DeleteCompositeOrphanHook(hook.Hook): |
348 class DeleteCompositeOrphanHook(hook.Hook): |
336 """delete the composed of a composite relation when this relation is deleted |
349 """delete the composed of a composite relation when this relation is deleted |
337 """ |
350 """ |