|
1 """Core hooks: check for data integrity according to the instance'schema |
|
2 validity |
|
3 |
|
4 :organization: Logilab |
|
5 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. |
|
6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
7 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses |
|
8 """ |
|
9 __docformat__ = "restructuredtext en" |
|
10 |
|
11 from cubicweb import ValidationError |
|
12 from cubicweb.selectors import entity_implements |
|
13 from cubicweb.server.hook import Hook |
|
14 from cubicweb.server.pool import LateOperation, PreCommitOperation |
|
15 from cubicweb.server.hookhelper import rproperty |
|
16 |
|
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) |
|
19 DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by', |
|
20 'is', 'is_instance_of', |
|
21 'wf_info_for', 'from_state', 'to_state')) |
|
22 DONT_CHECK_RTYPES_ON_DEL = set(('is', 'is_instance_of', |
|
23 'wf_info_for', 'from_state', 'to_state')) |
|
24 |
|
25 |
|
26 class _CheckRequiredRelationOperation(LateOperation): |
|
27 """checking relation cardinality has to be done after commit in |
|
28 case the relation is being replaced |
|
29 """ |
|
30 eid, rtype = None, None |
|
31 |
|
32 def precommit_event(self): |
|
33 # recheck pending eids |
|
34 if self.eid in self.session.transaction_data.get('pendingeids', ()): |
|
35 return |
|
36 if self.session.unsafe_execute(*self._rql()).rowcount < 1: |
|
37 etype = self.session.describe(self.eid)[0] |
|
38 _ = self.session._ |
|
39 msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)') |
|
40 msg %= {'rtype': _(self.rtype), 'etype': _(etype), 'eid': self.eid} |
|
41 raise ValidationError(self.eid, {self.rtype: msg}) |
|
42 |
|
43 def commit_event(self): |
|
44 pass |
|
45 |
|
46 def _rql(self): |
|
47 raise NotImplementedError() |
|
48 |
|
49 |
|
50 class _CheckSRelationOp(_CheckRequiredRelationOperation): |
|
51 """check required subject relation""" |
|
52 def _rql(self): |
|
53 return 'Any O WHERE S eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x' |
|
54 |
|
55 |
|
56 class _CheckORelationOp(_CheckRequiredRelationOperation): |
|
57 """check required object relation""" |
|
58 def _rql(self): |
|
59 return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x' |
|
60 |
|
61 |
|
62 class CheckCardinalityHook(Hook): |
|
63 """check cardinalities are satisfied""" |
|
64 __id__ = 'checkcard' |
|
65 category = 'integrity' |
|
66 events = ('after_add_entity', 'before_delete_relation') |
|
67 |
|
68 def __call__(self): |
|
69 getattr(self, self.event)() |
|
70 |
|
71 def checkrel_if_necessary(self, opcls, rtype, eid): |
|
72 """check an equivalent operation has not already been added""" |
|
73 for op in self.cw_req.pending_operations: |
|
74 if isinstance(op, opcls) and op.rtype == rtype and op.eid == eid: |
|
75 break |
|
76 else: |
|
77 opcls(self.cw_req, rtype=rtype, eid=eid) |
|
78 |
|
79 def after_add_entity(self): |
|
80 eid = self.entity.eid |
|
81 eschema = self.entity.e_schema |
|
82 for rschema, targetschemas, x in eschema.relation_definitions(): |
|
83 # skip automatically handled relations |
|
84 if rschema.type in DONT_CHECK_RTYPES_ON_ADD: |
|
85 continue |
|
86 if x == 'subject': |
|
87 subjtype = eschema |
|
88 objtype = targetschemas[0].type |
|
89 cardindex = 0 |
|
90 opcls = _CheckSRelationOp |
|
91 else: |
|
92 subjtype = targetschemas[0].type |
|
93 objtype = eschema |
|
94 cardindex = 1 |
|
95 opcls = _CheckORelationOp |
|
96 card = rschema.rproperty(subjtype, objtype, 'cardinality') |
|
97 if card[cardindex] in '1+': |
|
98 self.checkrel_if_necessary(opcls, rschema.type, eid) |
|
99 |
|
100 def before_delete_relation(self): |
|
101 rtype = self.rtype |
|
102 if rtype in DONT_CHECK_RTYPES_ON_DEL: |
|
103 return |
|
104 session = self.cw_req |
|
105 eidfrom, eidto = self.eidfrom, self.eidto |
|
106 card = rproperty(session, rtype, eidfrom, eidto, 'cardinality') |
|
107 pendingrdefs = session.transaction_data.get('pendingrdefs', ()) |
|
108 if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs: |
|
109 return |
|
110 pendingeids = session.transaction_data.get('pendingeids', ()) |
|
111 if card[0] in '1+' and not eidfrom in pendingeids: |
|
112 self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom) |
|
113 if card[1] in '1+' and not eidto in pendingeids: |
|
114 self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto) |
|
115 |
|
116 |
|
117 class _CheckConstraintsOp(LateOperation): |
|
118 """check a new relation satisfy its constraints |
|
119 """ |
|
120 def precommit_event(self): |
|
121 eidfrom, rtype, eidto = self.rdef |
|
122 # first check related entities have not been deleted in the same |
|
123 # transaction |
|
124 pending = self.session.transaction_data.get('pendingeids', ()) |
|
125 if eidfrom in pending: |
|
126 return |
|
127 if eidto in pending: |
|
128 return |
|
129 for constraint in self.constraints: |
|
130 try: |
|
131 constraint.repo_check(self.session, eidfrom, rtype, eidto) |
|
132 except NotImplementedError: |
|
133 self.critical('can\'t check constraint %s, not supported', |
|
134 constraint) |
|
135 |
|
136 def commit_event(self): |
|
137 pass |
|
138 |
|
139 |
|
140 class CheckConstraintHook(Hook): |
|
141 """check the relation satisfy its constraints |
|
142 |
|
143 this is delayed to a precommit time operation since other relation which |
|
144 will make constraint satisfied may be added later. |
|
145 """ |
|
146 __id__ = 'checkconstraint' |
|
147 category = 'integrity' |
|
148 events = ('after_add_relation',) |
|
149 def __call__(self): |
|
150 constraints = rproperty(self.cw_req, self.rtype, self.eidfrom, self.eidto, |
|
151 'constraints') |
|
152 if constraints: |
|
153 _CheckConstraintsOp(self.cw_req, constraints=constraints, |
|
154 rdef=(self.eidfrom, self.rtype, self.eidto)) |
|
155 |
|
156 class CheckUniqueHook(Hook): |
|
157 __id__ = 'checkunique' |
|
158 category = 'integrity' |
|
159 events = ('before_add_entity', 'before_update_entity') |
|
160 |
|
161 def __call__(self): |
|
162 entity = self.entity |
|
163 eschema = entity.e_schema |
|
164 for attr in entity.edited_attributes: |
|
165 val = entity[attr] |
|
166 if val is None: |
|
167 continue |
|
168 if eschema.subject_relation(attr).is_final() and \ |
|
169 eschema.has_unique_values(attr): |
|
170 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr) |
|
171 rset = self.cw_req.unsafe_execute(rql, {'val': val}) |
|
172 if rset and rset[0][0] != entity.eid: |
|
173 msg = self.cw_req._('the value "%s" is already used, use another one') |
|
174 raise ValidationError(entity.eid, {attr: msg % val}) |
|
175 |
|
176 |
|
177 class _DelayedDeleteOp(PreCommitOperation): |
|
178 """delete the object of composite relation except if the relation |
|
179 has actually been redirected to another composite |
|
180 """ |
|
181 |
|
182 def precommit_event(self): |
|
183 session = self.session |
|
184 # don't do anything if the entity is being created or deleted |
|
185 if not (self.eid in session.transaction_data.get('pendingeids', ()) or |
|
186 self.eid in session.transaction_data.get('neweids', ())): |
|
187 etype = session.describe(self.eid)[0] |
|
188 session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s' |
|
189 % (etype, self.relation), |
|
190 {'x': self.eid}, 'x') |
|
191 |
|
192 |
|
193 class DeleteCompositeOrphanHook(Hook): |
|
194 """delete the composed of a composite relation when this relation is deleted |
|
195 """ |
|
196 __id__ = 'deletecomposite' |
|
197 category = 'integrity' |
|
198 events = ('before_delete_relation',) |
|
199 def __call__(self): |
|
200 composite = rproperty(self.cw_req, self.rtype, self.eidfrom, self.eidto, |
|
201 'composite') |
|
202 if composite == 'subject': |
|
203 _DelayedDeleteOp(self.cw_req, eid=self.eidto, |
|
204 relation='Y %s X' % self.rtype) |
|
205 elif composite == 'object': |
|
206 _DelayedDeleteOp(self.cw_req, eid=self.eidfrom, |
|
207 relation='X %s Y' % self.rtype) |
|
208 |
|
209 |
|
210 class DontRemoveOwnersGroupHook(Hook): |
|
211 """delete the composed of a composite relation when this relation is deleted |
|
212 """ |
|
213 __id__ = 'checkownersgroup' |
|
214 __select__ = Hook.__select__ & entity_implements('CWGroup') |
|
215 category = 'integrity' |
|
216 events = ('before_delete_entity', 'before_update_entity') |
|
217 |
|
218 def __call__(self): |
|
219 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 elif self.event == 'before_update_entity' and 'name' in self.entity.edited_attribute: |
|
222 newname = self.entity.pop('name') |
|
223 oldname = self.entity.name |
|
224 if oldname == 'owners' and newname != oldname: |
|
225 raise ValidationError(self.entity.eid, {'name': self.cw_req._('can\'t be changed')}) |
|
226 self.entity['name'] = newname |
|
227 |
|
228 |