8 """ |
8 """ |
9 __docformat__ = "restructuredtext en" |
9 __docformat__ = "restructuredtext en" |
10 |
10 |
11 from cubicweb import ValidationError |
11 from cubicweb import ValidationError |
12 from cubicweb.selectors import entity_implements |
12 from cubicweb.selectors import entity_implements |
13 from cubicweb.server.hook import Hook |
13 from cubicweb.common.uilib import soup2xhtml |
|
14 from cubicweb.server import hook |
14 from cubicweb.server.pool import LateOperation, PreCommitOperation |
15 from cubicweb.server.pool import LateOperation, PreCommitOperation |
15 from cubicweb.server.hookhelper import rproperty |
|
16 |
16 |
17 # special relations that don't have to be checked for integrity, usually |
17 # special relations that don't have to be checked for integrity, usually |
18 # because they are handled internally by hooks (so we trust ourselves) |
18 # because they are handled internally by hooks (so we trust ourselves) |
19 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by', |
19 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by', |
20 'is', 'is_instance_of', |
20 'is', 'is_instance_of', |
21 'wf_info_for', 'from_state', 'to_state')) |
21 'wf_info_for', 'from_state', 'to_state')) |
22 DONT_CHECK_RTYPES_ON_DEL = set(('is', 'is_instance_of', |
22 DONT_CHECK_RTYPES_ON_DEL = set(('is', 'is_instance_of', |
23 'wf_info_for', 'from_state', 'to_state')) |
23 'wf_info_for', 'from_state', 'to_state')) |
24 |
24 |
25 |
25 |
26 class _CheckRequiredRelationOperation(LateOperation): |
26 class _CheckRequiredRelationOperation(hook.LateOperation): |
27 """checking relation cardinality has to be done after commit in |
27 """checking relation cardinality has to be done after commit in |
28 case the relation is being replaced |
28 case the relation is being replaced |
29 """ |
29 """ |
30 eid, rtype = None, None |
30 eid, rtype = None, None |
31 |
31 |
32 def precommit_event(self): |
32 def precommit_event(self): |
33 # recheck pending eids |
33 # recheck pending eids |
34 if self.eid in self.session.transaction_data.get('pendingeids', ()): |
34 if self.session.deleted_in_transaction(self.eid): |
35 return |
35 return |
36 if self.session.unsafe_execute(*self._rql()).rowcount < 1: |
36 if self.session.unsafe_execute(*self._rql()).rowcount < 1: |
37 etype = self.session.describe(self.eid)[0] |
37 etype = self.session.describe(self.eid)[0] |
38 _ = self.session._ |
38 _ = self.session._ |
39 msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)') |
39 msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)') |
57 """check required object relation""" |
57 """check required object relation""" |
58 def _rql(self): |
58 def _rql(self): |
59 return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x' |
59 return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x' |
60 |
60 |
61 |
61 |
62 class CheckCardinalityHook(Hook): |
62 class IntegrityHook(hook.Hook): |
|
63 __abstract__ = True |
|
64 category = 'integrity' |
|
65 |
|
66 |
|
67 class CheckCardinalityHook(IntegrityHook): |
63 """check cardinalities are satisfied""" |
68 """check cardinalities are satisfied""" |
64 __id__ = 'checkcard' |
69 __id__ = 'checkcard' |
65 category = 'integrity' |
|
66 events = ('after_add_entity', 'before_delete_relation') |
70 events = ('after_add_entity', 'before_delete_relation') |
67 |
71 |
68 def __call__(self): |
72 def __call__(self): |
69 getattr(self, self.event)() |
73 getattr(self, self.event)() |
70 |
74 |
101 rtype = self.rtype |
105 rtype = self.rtype |
102 if rtype in DONT_CHECK_RTYPES_ON_DEL: |
106 if rtype in DONT_CHECK_RTYPES_ON_DEL: |
103 return |
107 return |
104 session = self.cw_req |
108 session = self.cw_req |
105 eidfrom, eidto = self.eidfrom, self.eidto |
109 eidfrom, eidto = self.eidfrom, self.eidto |
106 card = rproperty(session, rtype, eidfrom, eidto, 'cardinality') |
110 card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality') |
107 pendingrdefs = session.transaction_data.get('pendingrdefs', ()) |
111 pendingrdefs = session.transaction_data.get('pendingrdefs', ()) |
108 if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs: |
112 if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs: |
109 return |
113 return |
110 pendingeids = session.transaction_data.get('pendingeids', ()) |
114 if card[0] in '1+' and not session.deleted_in_transaction(eidfrom): |
111 if card[0] in '1+' and not eidfrom in pendingeids: |
|
112 self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom) |
115 self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom) |
113 if card[1] in '1+' and not eidto in pendingeids: |
116 if card[1] in '1+' and not session.deleted_in_transaction(eidto): |
114 self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto) |
117 self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto) |
115 |
118 |
116 |
119 |
117 class _CheckConstraintsOp(LateOperation): |
120 class _CheckConstraintsOp(hook.LateOperation): |
118 """check a new relation satisfy its constraints |
121 """check a new relation satisfy its constraints |
119 """ |
122 """ |
120 def precommit_event(self): |
123 def precommit_event(self): |
121 eidfrom, rtype, eidto = self.rdef |
124 eidfrom, rtype, eidto = self.rdef |
122 # first check related entities have not been deleted in the same |
125 # first check related entities have not been deleted in the same |
123 # transaction |
126 # transaction |
124 pending = self.session.transaction_data.get('pendingeids', ()) |
127 if self.session.deleted_in_transaction(eidfrom): |
125 if eidfrom in pending: |
128 return |
126 return |
129 if self.session.deleted_in_transaction(eidto): |
127 if eidto in pending: |
|
128 return |
130 return |
129 for constraint in self.constraints: |
131 for constraint in self.constraints: |
130 try: |
132 try: |
131 constraint.repo_check(self.session, eidfrom, rtype, eidto) |
133 constraint.repo_check(self.session, eidfrom, rtype, eidto) |
132 except NotImplementedError: |
134 except NotImplementedError: |
135 |
137 |
136 def commit_event(self): |
138 def commit_event(self): |
137 pass |
139 pass |
138 |
140 |
139 |
141 |
140 class CheckConstraintHook(Hook): |
142 class CheckConstraintHook(IntegrityHook): |
141 """check the relation satisfy its constraints |
143 """check the relation satisfy its constraints |
142 |
144 |
143 this is delayed to a precommit time operation since other relation which |
145 this is delayed to a precommit time operation since other relation which |
144 will make constraint satisfied may be added later. |
146 will make constraint satisfied may be added later. |
145 """ |
147 """ |
146 __id__ = 'checkconstraint' |
148 __id__ = 'checkconstraint' |
147 category = 'integrity' |
|
148 events = ('after_add_relation',) |
149 events = ('after_add_relation',) |
149 def __call__(self): |
150 |
150 constraints = rproperty(self.cw_req, self.rtype, self.eidfrom, self.eidto, |
151 def __call__(self): |
|
152 constraints = self.cw_req.schema_rproperty(self.rtype, self.eidfrom, self.eidto, |
151 'constraints') |
153 'constraints') |
152 if constraints: |
154 if constraints: |
153 _CheckConstraintsOp(self.cw_req, constraints=constraints, |
155 _CheckConstraintsOp(self.cw_req, constraints=constraints, |
154 rdef=(self.eidfrom, self.rtype, self.eidto)) |
156 rdef=(self.eidfrom, self.rtype, self.eidto)) |
155 |
157 |
156 class CheckUniqueHook(Hook): |
158 |
|
159 class CheckUniqueHook(IntegrityHook): |
157 __id__ = 'checkunique' |
160 __id__ = 'checkunique' |
158 category = 'integrity' |
|
159 events = ('before_add_entity', 'before_update_entity') |
161 events = ('before_add_entity', 'before_update_entity') |
160 |
162 |
161 def __call__(self): |
163 def __call__(self): |
162 entity = self.entity |
164 entity = self.entity |
163 eschema = entity.e_schema |
165 eschema = entity.e_schema |
172 if rset and rset[0][0] != entity.eid: |
174 if rset and rset[0][0] != entity.eid: |
173 msg = self.cw_req._('the value "%s" is already used, use another one') |
175 msg = self.cw_req._('the value "%s" is already used, use another one') |
174 raise ValidationError(entity.eid, {attr: msg % val}) |
176 raise ValidationError(entity.eid, {attr: msg % val}) |
175 |
177 |
176 |
178 |
177 class _DelayedDeleteOp(PreCommitOperation): |
179 class _DelayedDeleteOp(hook.Operation): |
178 """delete the object of composite relation except if the relation |
180 """delete the object of composite relation except if the relation |
179 has actually been redirected to another composite |
181 has actually been redirected to another composite |
180 """ |
182 """ |
181 |
183 |
182 def precommit_event(self): |
184 def precommit_event(self): |
183 session = self.session |
185 session = self.session |
184 # don't do anything if the entity is being created or deleted |
186 # don't do anything if the entity is being created or deleted |
185 if not (self.eid in session.transaction_data.get('pendingeids', ()) or |
187 if not (session.deleted_in_transaction(self.eid) or |
186 self.eid in session.transaction_data.get('neweids', ())): |
188 session.added_in_transaction(self.eid)): |
187 etype = session.describe(self.eid)[0] |
189 etype = session.describe(self.eid)[0] |
188 session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s' |
190 session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s' |
189 % (etype, self.relation), |
191 % (etype, self.relation), |
190 {'x': self.eid}, 'x') |
192 {'x': self.eid}, 'x') |
191 |
193 |
192 |
194 |
193 class DeleteCompositeOrphanHook(Hook): |
195 class DeleteCompositeOrphanHook(IntegrityHook): |
194 """delete the composed of a composite relation when this relation is deleted |
196 """delete the composed of a composite relation when this relation is deleted |
195 """ |
197 """ |
196 __id__ = 'deletecomposite' |
198 __id__ = 'deletecomposite' |
197 category = 'integrity' |
|
198 events = ('before_delete_relation',) |
199 events = ('before_delete_relation',) |
199 def __call__(self): |
200 |
200 composite = rproperty(self.cw_req, self.rtype, self.eidfrom, self.eidto, |
201 def __call__(self): |
201 'composite') |
202 composite = self.cw_req.schema_rproperty(self.rtype, self.eidfrom, self.eidto, |
|
203 'composite') |
202 if composite == 'subject': |
204 if composite == 'subject': |
203 _DelayedDeleteOp(self.cw_req, eid=self.eidto, |
205 _DelayedDeleteOp(self.cw_req, eid=self.eidto, |
204 relation='Y %s X' % self.rtype) |
206 relation='Y %s X' % self.rtype) |
205 elif composite == 'object': |
207 elif composite == 'object': |
206 _DelayedDeleteOp(self.cw_req, eid=self.eidfrom, |
208 _DelayedDeleteOp(self.cw_req, eid=self.eidfrom, |
207 relation='X %s Y' % self.rtype) |
209 relation='X %s Y' % self.rtype) |
208 |
210 |
209 |
211 |
210 class DontRemoveOwnersGroupHook(Hook): |
212 class DontRemoveOwnersGroupHook(IntegrityHook): |
211 """delete the composed of a composite relation when this relation is deleted |
213 """delete the composed of a composite relation when this relation is deleted |
212 """ |
214 """ |
213 __id__ = 'checkownersgroup' |
215 __id__ = 'checkownersgroup' |
214 __select__ = Hook.__select__ & entity_implements('CWGroup') |
216 __select__ = IntegrityHook.__select__ & entity_implements('CWGroup') |
215 category = 'integrity' |
|
216 events = ('before_delete_entity', 'before_update_entity') |
217 events = ('before_delete_entity', 'before_update_entity') |
217 |
218 |
218 def __call__(self): |
219 def __call__(self): |
219 if self.event == 'before_delete_entity' and self.entity.name == 'owners': |
220 if self.event == 'before_delete_entity' and self.entity.name == 'owners': |
220 raise ValidationError(self.entity.eid, {None: self.cw_req._('can\'t be deleted')}) |
221 raise ValidationError(self.entity.eid, {None: self.cw_req._('can\'t be deleted')}) |
224 if oldname == 'owners' and newname != oldname: |
225 if oldname == 'owners' and newname != oldname: |
225 raise ValidationError(self.entity.eid, {'name': self.cw_req._('can\'t be changed')}) |
226 raise ValidationError(self.entity.eid, {'name': self.cw_req._('can\'t be changed')}) |
226 self.entity['name'] = newname |
227 self.entity['name'] = newname |
227 |
228 |
228 |
229 |
|
230 class TidyHtmlFields(IntegrityHook): |
|
231 """tidy HTML in rich text strings""" |
|
232 __id__ = 'htmltidy' |
|
233 events = ('before_add_entity', 'before_update_entity') |
|
234 |
|
235 def __call__(self): |
|
236 entity = self.entity |
|
237 metaattrs = entity.e_schema.meta_attributes() |
|
238 for metaattr, (metadata, attr) in metaattrs.iteritems(): |
|
239 if metadata == 'format' and attr in entity.edited_attributes: |
|
240 try: |
|
241 value = entity[attr] |
|
242 except KeyError: |
|
243 continue # no text to tidy |
|
244 if isinstance(value, unicode): # filter out None and Binary |
|
245 if getattr(entity, str(metaattr)) == 'text/html': |
|
246 entity[attr] = soup2xhtml(value, self.cw_req.encoding) |
|
247 |
|
248 |
|
249 class StripCWUserLoginHook(IntegrityHook): |
|
250 """ensure user logins are stripped""" |
|
251 __id__ = 'stripuserlogin' |
|
252 __select__ = IntegrityHook.__select__ & entity_implements('CWUser') |
|
253 events = ('before_add_entity', 'before_update_entity',) |
|
254 |
|
255 def call(self, session, entity): |
|
256 if 'login' in entity.edited_attributes and entity['login']: |
|
257 entity['login'] = entity['login'].strip() |