4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
6 """ |
6 """ |
7 __docformat__ = "restructuredtext en" |
7 __docformat__ = "restructuredtext en" |
8 |
8 |
9 import warnings |
|
10 import re |
9 import re |
11 from logging import getLogger |
10 from logging import getLogger |
12 |
11 from warnings import warn |
13 from logilab.common.decorators import cached, clear_cache |
12 |
|
13 from logilab.common.decorators import cached, clear_cache, monkeypatch |
14 from logilab.common.compat import any |
14 from logilab.common.compat import any |
15 |
15 |
16 from yams import BadSchemaDefinition, buildobjs as ybo |
16 from yams import BadSchemaDefinition, buildobjs as ybo |
17 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema |
17 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema |
18 from yams.constraints import BaseConstraint, StaticVocabularyConstraint |
18 from yams.constraints import BaseConstraint, StaticVocabularyConstraint |
20 SchemaLoader) |
20 SchemaLoader) |
21 |
21 |
22 from rql import parse, nodes, RQLSyntaxError, TypeResolverException |
22 from rql import parse, nodes, RQLSyntaxError, TypeResolverException |
23 |
23 |
24 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized |
24 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized |
|
25 from cubicweb import set_log_methods |
|
26 |
|
27 # XXX <3.2 bw compat |
|
28 from yams import schema |
|
29 schema.use_py_datetime() |
|
30 nodes.use_py_datetime() |
25 |
31 |
26 _ = unicode |
32 _ = unicode |
27 |
33 |
28 BASEGROUPS = ('managers', 'users', 'guests', 'owners') |
34 BASEGROUPS = ('managers', 'users', 'guests', 'owners') |
29 |
35 |
34 ybo.RTYPE_PROPERTIES += ('eid',) |
40 ybo.RTYPE_PROPERTIES += ('eid',) |
35 ybo.RDEF_PROPERTIES += ('eid',) |
41 ybo.RDEF_PROPERTIES += ('eid',) |
36 |
42 |
37 def bw_normalize_etype(etype): |
43 def bw_normalize_etype(etype): |
38 if etype in ETYPE_NAME_MAP: |
44 if etype in ETYPE_NAME_MAP: |
39 from warnings import warn |
|
40 msg = '%s has been renamed to %s, please update your code' % ( |
45 msg = '%s has been renamed to %s, please update your code' % ( |
41 etype, ETYPE_NAME_MAP[etype]) |
46 etype, ETYPE_NAME_MAP[etype]) |
42 warn(msg, DeprecationWarning, stacklevel=4) |
47 warn(msg, DeprecationWarning, stacklevel=4) |
43 etype = ETYPE_NAME_MAP[etype] |
48 etype = ETYPE_NAME_MAP[etype] |
44 return etype |
49 return etype |
45 |
50 |
46 # monkey path yams.builder.RelationDefinition to support a new wildcard type '@' |
51 # monkey path yams.builder.RelationDefinition to support a new wildcard type '@' |
66 etypes += tuple(system_etypes(schema)) |
71 etypes += tuple(system_etypes(schema)) |
67 return etypes |
72 return etypes |
68 return (etype,) |
73 return (etype,) |
69 ybo.RelationDefinition._actual_types = _actual_types |
74 ybo.RelationDefinition._actual_types = _actual_types |
70 |
75 |
|
76 |
|
77 ## cubicweb provides a RichString class for convenience |
|
78 class RichString(ybo.String): |
|
79 """Convenience RichString attribute type |
|
80 The following declaration:: |
|
81 |
|
82 class Card(EntityType): |
|
83 content = RichString(fulltextindexed=True, default_format='text/rest') |
|
84 |
|
85 is equivalent to:: |
|
86 |
|
87 class Card(EntityType): |
|
88 content_format = String(meta=True, internationalizable=True, |
|
89 default='text/rest', constraints=[format_constraint]) |
|
90 content = String(fulltextindexed=True) |
|
91 """ |
|
92 def __init__(self, default_format='text/plain', format_constraints=None, **kwargs): |
|
93 self.default_format = default_format |
|
94 self.format_constraints = format_constraints or [format_constraint] |
|
95 super(RichString, self).__init__(**kwargs) |
|
96 |
|
97 PyFileReader.context['RichString'] = RichString |
|
98 |
|
99 ## need to monkeypatch yams' _add_relation function to handle RichString |
|
100 yams_add_relation = ybo._add_relation |
|
101 @monkeypatch(ybo) |
|
102 def _add_relation(relations, rdef, name=None, insertidx=None): |
|
103 if isinstance(rdef, RichString): |
|
104 format_attrdef = ybo.String(meta=True, internationalizable=True, |
|
105 default=rdef.default_format, maxsize=50, |
|
106 constraints=rdef.format_constraints) |
|
107 yams_add_relation(relations, format_attrdef, name+'_format', insertidx) |
|
108 yams_add_relation(relations, rdef, name, insertidx) |
|
109 |
71 def display_name(req, key, form=''): |
110 def display_name(req, key, form=''): |
72 """return a internationalized string for the key (schema entity or relation |
111 """return a internationalized string for the key (schema entity or relation |
73 name) in a given form |
112 name) in a given form |
74 """ |
113 """ |
75 assert form in ('', 'plural', 'subject', 'object') |
114 assert form in ('', 'plural', 'subject', 'object') |
217 super(CubicWebEntitySchema, self).__init__(schema, edef, **kwargs) |
256 super(CubicWebEntitySchema, self).__init__(schema, edef, **kwargs) |
218 if eid is None and edef is not None: |
257 if eid is None and edef is not None: |
219 eid = getattr(edef, 'eid', None) |
258 eid = getattr(edef, 'eid', None) |
220 self.eid = eid |
259 self.eid = eid |
221 # take care: no _groups attribute when deep-copying |
260 # take care: no _groups attribute when deep-copying |
222 if getattr(self, '_groups', None): |
261 if getattr(self, '_groups', None): |
223 for groups in self._groups.itervalues(): |
262 for groups in self._groups.itervalues(): |
224 for group_or_rqlexpr in groups: |
263 for group_or_rqlexpr in groups: |
225 if isinstance(group_or_rqlexpr, RRQLExpression): |
264 if isinstance(group_or_rqlexpr, RRQLExpression): |
226 msg = "can't use RRQLExpression on an entity type, use an ERQLExpression (%s)" |
265 msg = "can't use RRQLExpression on an entity type, use an ERQLExpression (%s)" |
227 raise BadSchemaDefinition(msg % self.type) |
266 raise BadSchemaDefinition(msg % self.type) |
228 |
267 |
229 def attribute_definitions(self): |
268 def attribute_definitions(self): |
230 """return an iterator on attribute definitions |
269 """return an iterator on attribute definitions |
231 |
270 |
232 attribute relations are a subset of subject relations where the |
271 attribute relations are a subset of subject relations where the |
233 object's type is a final entity |
272 object's type is a final entity |
234 |
273 |
235 an attribute definition is a 2-uple : |
274 an attribute definition is a 2-uple : |
236 * name of the relation |
275 * name of the relation |
237 * schema of the destination entity type |
276 * schema of the destination entity type |
238 """ |
277 """ |
239 iter = super(CubicWebEntitySchema, self).attribute_definitions() |
278 iter = super(CubicWebEntitySchema, self).attribute_definitions() |
240 for rschema, attrschema in iter: |
279 for rschema, attrschema in iter: |
241 if rschema.type == 'has_text': |
280 if rschema.type == 'has_text': |
242 continue |
281 continue |
243 yield rschema, attrschema |
282 yield rschema, attrschema |
244 |
283 |
245 def add_subject_relation(self, rschema): |
284 def add_subject_relation(self, rschema): |
246 """register the relation schema as possible subject relation""" |
285 """register the relation schema as possible subject relation""" |
247 super(CubicWebEntitySchema, self).add_subject_relation(rschema) |
286 super(CubicWebEntitySchema, self).add_subject_relation(rschema) |
248 self._update_has_text() |
287 self._update_has_text() |
249 |
288 |
250 def del_subject_relation(self, rtype): |
289 def del_subject_relation(self, rtype): |
251 super(CubicWebEntitySchema, self).del_subject_relation(rtype) |
290 super(CubicWebEntitySchema, self).del_subject_relation(rtype) |
252 self._update_has_text(False) |
291 self._update_has_text(False) |
253 |
292 |
254 def _update_has_text(self, need_has_text=None): |
293 def _update_has_text(self, need_has_text=None): |
255 may_need_has_text, has_has_text = False, False |
294 may_need_has_text, has_has_text = False, False |
256 for rschema in self.subject_relations(): |
295 for rschema in self.subject_relations(): |
257 if rschema.is_final(): |
296 if rschema.is_final(): |
258 if rschema == 'has_text': |
297 if rschema == 'has_text': |
276 if need_has_text and not has_has_text: |
315 if need_has_text and not has_has_text: |
277 rdef = ybo.RelationDefinition(self.type, 'has_text', 'String') |
316 rdef = ybo.RelationDefinition(self.type, 'has_text', 'String') |
278 self.schema.add_relation_def(rdef) |
317 self.schema.add_relation_def(rdef) |
279 elif not need_has_text and has_has_text: |
318 elif not need_has_text and has_has_text: |
280 self.schema.del_relation_def(self.type, 'has_text', 'String') |
319 self.schema.del_relation_def(self.type, 'has_text', 'String') |
281 |
320 |
282 def schema_entity(self): |
321 def schema_entity(self): |
283 """return True if this entity type is used to build the schema""" |
322 """return True if this entity type is used to build the schema""" |
284 return self.type in self.schema.schema_entity_types() |
323 return self.type in self.schema.schema_entity_types() |
285 |
324 |
286 def rich_text_fields(self): |
|
287 """return an iterator on (attribute, format attribute) of rich text field |
|
288 |
|
289 (the first tuple element containing the text and the second the text format) |
|
290 """ |
|
291 for rschema, _ in self.attribute_definitions(): |
|
292 if rschema.type.endswith('_format'): |
|
293 for constraint in self.constraints(rschema): |
|
294 if isinstance(constraint, FormatConstraint): |
|
295 yield self.subject_relation(rschema.type[:-7]), rschema |
|
296 break |
|
297 |
|
298 def check_perm(self, session, action, eid=None): |
325 def check_perm(self, session, action, eid=None): |
299 # NB: session may be a server session or a request object |
326 # NB: session may be a server session or a request object |
300 user = session.user |
327 user = session.user |
301 # check user is in an allowed group, if so that's enough |
328 # check user is in an allowed group, if so that's enough |
302 # internal sessions should always stop there |
329 # internal sessions should always stop there |
308 user.owns(eid): |
335 user.owns(eid): |
309 return |
336 return |
310 # else if there is some rql expressions, check them |
337 # else if there is some rql expressions, check them |
311 if any(rqlexpr.check(session, eid) |
338 if any(rqlexpr.check(session, eid) |
312 for rqlexpr in self.get_rqlexprs(action)): |
339 for rqlexpr in self.get_rqlexprs(action)): |
313 return |
340 return |
314 raise Unauthorized(action, str(self)) |
341 raise Unauthorized(action, str(self)) |
315 |
342 |
316 def rql_expression(self, expression, mainvars=None, eid=None): |
343 def rql_expression(self, expression, mainvars=None, eid=None): |
317 """rql expression factory""" |
344 """rql expression factory""" |
318 return ERQLExpression(expression, mainvars, eid) |
345 return ERQLExpression(expression, mainvars, eid) |
319 |
346 |
320 class CubicWebRelationSchema(RelationSchema): |
347 class CubicWebRelationSchema(RelationSchema): |
321 RelationSchema._RPROPERTIES['eid'] = None |
348 RelationSchema._RPROPERTIES['eid'] = None |
322 _perms_checked = False |
349 _perms_checked = False |
323 |
350 |
324 def __init__(self, schema=None, rdef=None, eid=None, **kwargs): |
351 def __init__(self, schema=None, rdef=None, eid=None, **kwargs): |
325 if rdef is not None: |
352 if rdef is not None: |
326 # if this relation is inlined |
353 # if this relation is inlined |
327 self.inlined = rdef.inlined |
354 self.inlined = rdef.inlined |
328 super(CubicWebRelationSchema, self).__init__(schema, rdef, **kwargs) |
355 super(CubicWebRelationSchema, self).__init__(schema, rdef, **kwargs) |
329 if eid is None and rdef is not None: |
356 if eid is None and rdef is not None: |
330 eid = getattr(rdef, 'eid', None) |
357 eid = getattr(rdef, 'eid', None) |
331 self.eid = eid |
358 self.eid = eid |
332 |
359 |
333 |
360 |
334 def update(self, subjschema, objschema, rdef): |
361 def update(self, subjschema, objschema, rdef): |
335 super(CubicWebRelationSchema, self).update(subjschema, objschema, rdef) |
362 super(CubicWebRelationSchema, self).update(subjschema, objschema, rdef) |
336 if not self._perms_checked and self._groups: |
363 if not self._perms_checked and self._groups: |
337 for action, groups in self._groups.iteritems(): |
364 for action, groups in self._groups.iteritems(): |
338 for group_or_rqlexpr in groups: |
365 for group_or_rqlexpr in groups: |
348 rqlexpr = group_or_rqlexpr |
375 rqlexpr = group_or_rqlexpr |
349 newrqlexprs = [x for x in self.get_rqlexprs(action) if not x is rqlexpr] |
376 newrqlexprs = [x for x in self.get_rqlexprs(action) if not x is rqlexpr] |
350 newrqlexprs.append(ERQLExpression(rqlexpr.expression, |
377 newrqlexprs.append(ERQLExpression(rqlexpr.expression, |
351 rqlexpr.mainvars, |
378 rqlexpr.mainvars, |
352 rqlexpr.eid)) |
379 rqlexpr.eid)) |
353 self.set_rqlexprs(action, newrqlexprs) |
380 self.set_rqlexprs(action, newrqlexprs) |
354 else: |
381 else: |
355 msg = "can't use RRQLExpression on a final relation "\ |
382 msg = "can't use RRQLExpression on a final relation "\ |
356 "type (eg attribute relation), use an ERQLExpression (%s)" |
383 "type (eg attribute relation), use an ERQLExpression (%s)" |
357 raise BadSchemaDefinition(msg % self.type) |
384 raise BadSchemaDefinition(msg % self.type) |
358 elif not self.final and \ |
385 elif not self.final and \ |
359 isinstance(group_or_rqlexpr, ERQLExpression): |
386 isinstance(group_or_rqlexpr, ERQLExpression): |
360 msg = "can't use ERQLExpression on a relation type, use "\ |
387 msg = "can't use ERQLExpression on a relation type, use "\ |
361 "a RRQLExpression (%s)" |
388 "a RRQLExpression (%s)" |
362 raise BadSchemaDefinition(msg % self.type) |
389 raise BadSchemaDefinition(msg % self.type) |
363 self._perms_checked = True |
390 self._perms_checked = True |
364 |
391 |
365 def cardinality(self, subjtype, objtype, target): |
392 def cardinality(self, subjtype, objtype, target): |
366 card = self.rproperty(subjtype, objtype, 'cardinality') |
393 card = self.rproperty(subjtype, objtype, 'cardinality') |
367 return (target == 'subject' and card[0]) or \ |
394 return (target == 'subject' and card[0]) or \ |
368 (target == 'object' and card[1]) |
395 (target == 'object' and card[1]) |
369 |
396 |
370 def schema_relation(self): |
397 def schema_relation(self): |
371 return self.type in ('relation_type', 'from_entity', 'to_entity', |
398 return self.type in ('relation_type', 'from_entity', 'to_entity', |
372 'constrained_by', 'cstrtype') |
399 'constrained_by', 'cstrtype') |
373 |
400 |
374 def physical_mode(self): |
401 def physical_mode(self): |
375 """return an appropriate mode for physical storage of this relation type: |
402 """return an appropriate mode for physical storage of this relation type: |
376 * 'subjectinline' if every possible subject cardinalities are 1 or ? |
403 * 'subjectinline' if every possible subject cardinalities are 1 or ? |
377 * 'objectinline' if 'subjectinline' mode is not possible but every |
404 * 'objectinline' if 'subjectinline' mode is not possible but every |
378 possible object cardinalities are 1 or ? |
405 possible object cardinalities are 1 or ? |
384 def check_perm(self, session, action, *args, **kwargs): |
411 def check_perm(self, session, action, *args, **kwargs): |
385 # NB: session may be a server session or a request object check user is |
412 # NB: session may be a server session or a request object check user is |
386 # in an allowed group, if so that's enough internal sessions should |
413 # in an allowed group, if so that's enough internal sessions should |
387 # always stop there |
414 # always stop there |
388 if session.user.matching_groups(self.get_groups(action)): |
415 if session.user.matching_groups(self.get_groups(action)): |
389 return |
416 return |
390 # else if there is some rql expressions, check them |
417 # else if there is some rql expressions, check them |
391 if any(rqlexpr.check(session, *args, **kwargs) |
418 if any(rqlexpr.check(session, *args, **kwargs) |
392 for rqlexpr in self.get_rqlexprs(action)): |
419 for rqlexpr in self.get_rqlexprs(action)): |
393 return |
420 return |
394 raise Unauthorized(action, str(self)) |
421 raise Unauthorized(action, str(self)) |
397 """rql expression factory""" |
424 """rql expression factory""" |
398 if self.is_final(): |
425 if self.is_final(): |
399 return ERQLExpression(expression, mainvars, eid) |
426 return ERQLExpression(expression, mainvars, eid) |
400 return RRQLExpression(expression, mainvars, eid) |
427 return RRQLExpression(expression, mainvars, eid) |
401 |
428 |
402 |
429 |
403 class CubicWebSchema(Schema): |
430 class CubicWebSchema(Schema): |
404 """set of entities and relations schema defining the possible data sets |
431 """set of entities and relations schema defining the possible data sets |
405 used in an application |
432 used in an application |
406 |
433 |
407 |
434 |
408 :type name: str |
435 :type name: str |
409 :ivar name: name of the schema, usually the application identifier |
436 :ivar name: name of the schema, usually the application identifier |
410 |
437 |
411 :type base: str |
438 :type base: str |
412 :ivar base: path of the directory where the schema is defined |
439 :ivar base: path of the directory where the schema is defined |
413 """ |
440 """ |
414 reading_from_database = False |
441 reading_from_database = False |
415 entity_class = CubicWebEntitySchema |
442 entity_class = CubicWebEntitySchema |
416 relation_class = CubicWebRelationSchema |
443 relation_class = CubicWebRelationSchema |
417 |
444 |
418 def __init__(self, *args, **kwargs): |
445 def __init__(self, *args, **kwargs): |
419 self._eid_index = {} |
446 self._eid_index = {} |
426 rschema.final = True |
453 rschema.final = True |
427 rschema.set_default_groups() |
454 rschema.set_default_groups() |
428 rschema = self.add_relation_type(ybo.RelationType('identity', meta=True)) |
455 rschema = self.add_relation_type(ybo.RelationType('identity', meta=True)) |
429 rschema.final = False |
456 rschema.final = False |
430 rschema.set_default_groups() |
457 rschema.set_default_groups() |
431 |
458 |
432 def schema_entity_types(self): |
459 def schema_entity_types(self): |
433 """return the list of entity types used to build the schema""" |
460 """return the list of entity types used to build the schema""" |
434 return frozenset(('EEType', 'ERType', 'EFRDef', 'ENFRDef', |
461 return frozenset(('CWEType', 'CWRType', 'CWAttribute', 'CWRelation', |
435 'EConstraint', 'EConstraintType', 'RQLExpression', |
462 'CWConstraint', 'CWConstraintType', 'RQLExpression', |
436 # XXX those are not really "schema" entity types |
463 # XXX those are not really "schema" entity types |
437 # but we usually don't want them as @* targets |
464 # but we usually don't want them as @* targets |
438 'EProperty', 'EPermission', 'State', 'Transition')) |
465 'CWProperty', 'CWPermission', 'State', 'Transition')) |
439 |
466 |
440 def add_entity_type(self, edef): |
467 def add_entity_type(self, edef): |
441 edef.name = edef.name.encode() |
468 edef.name = edef.name.encode() |
442 edef.name = bw_normalize_etype(edef.name) |
469 edef.name = bw_normalize_etype(edef.name) |
443 assert re.match(r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*$', edef.name), repr(edef.name) |
470 assert re.match(r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*$', edef.name), repr(edef.name) |
444 eschema = super(CubicWebSchema, self).add_entity_type(edef) |
471 eschema = super(CubicWebSchema, self).add_entity_type(edef) |
449 self.add_relation_def(rdef) |
476 self.add_relation_def(rdef) |
450 rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type) |
477 rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type) |
451 self.add_relation_def(rdef) |
478 self.add_relation_def(rdef) |
452 self._eid_index[eschema.eid] = eschema |
479 self._eid_index[eschema.eid] = eschema |
453 return eschema |
480 return eschema |
454 |
481 |
455 def add_relation_type(self, rdef): |
482 def add_relation_type(self, rdef): |
456 rdef.name = rdef.name.lower().encode() |
483 rdef.name = rdef.name.lower().encode() |
457 rschema = super(CubicWebSchema, self).add_relation_type(rdef) |
484 rschema = super(CubicWebSchema, self).add_relation_type(rdef) |
458 self._eid_index[rschema.eid] = rschema |
485 self._eid_index[rschema.eid] = rschema |
459 return rschema |
486 return rschema |
460 |
487 |
461 def add_relation_def(self, rdef): |
488 def add_relation_def(self, rdef): |
462 """build a part of a relation schema |
489 """build a part of a relation schema |
463 (i.e. add a relation between two specific entity's types) |
490 (i.e. add a relation between two specific entity's types) |
464 |
491 |
465 :type subject: str |
492 :type subject: str |
475 :param: the newly created or just completed relation schema |
502 :param: the newly created or just completed relation schema |
476 """ |
503 """ |
477 rdef.name = rdef.name.lower() |
504 rdef.name = rdef.name.lower() |
478 rdef.subject = bw_normalize_etype(rdef.subject) |
505 rdef.subject = bw_normalize_etype(rdef.subject) |
479 rdef.object = bw_normalize_etype(rdef.object) |
506 rdef.object = bw_normalize_etype(rdef.object) |
480 super(CubicWebSchema, self).add_relation_def(rdef) |
507 if super(CubicWebSchema, self).add_relation_def(rdef): |
481 try: |
508 try: |
482 self._eid_index[rdef.eid] = (self.eschema(rdef.subject), |
509 self._eid_index[rdef.eid] = (self.eschema(rdef.subject), |
483 self.rschema(rdef.name), |
510 self.rschema(rdef.name), |
484 self.eschema(rdef.object)) |
511 self.eschema(rdef.object)) |
485 except AttributeError: |
512 except AttributeError: |
486 pass # not a serialized schema |
513 pass # not a serialized schema |
487 |
514 |
488 def del_relation_type(self, rtype): |
515 def del_relation_type(self, rtype): |
489 rschema = self.rschema(rtype) |
516 rschema = self.rschema(rtype) |
490 self._eid_index.pop(rschema.eid, None) |
517 self._eid_index.pop(rschema.eid, None) |
491 super(CubicWebSchema, self).del_relation_type(rtype) |
518 super(CubicWebSchema, self).del_relation_type(rtype) |
492 |
519 |
493 def del_relation_def(self, subjtype, rtype, objtype): |
520 def del_relation_def(self, subjtype, rtype, objtype): |
494 for k, v in self._eid_index.items(): |
521 for k, v in self._eid_index.items(): |
495 if v == (subjtype, rtype, objtype): |
522 if v == (subjtype, rtype, objtype): |
496 del self._eid_index[k] |
523 del self._eid_index[k] |
497 super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype) |
524 super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype) |
498 |
525 |
499 def del_entity_type(self, etype): |
526 def del_entity_type(self, etype): |
500 eschema = self.eschema(etype) |
527 eschema = self.eschema(etype) |
501 self._eid_index.pop(eschema.eid, None) |
528 self._eid_index.pop(eschema.eid, None) |
502 # deal with has_text first, else its automatic deletion (see above) |
529 # deal with has_text first, else its automatic deletion (see above) |
503 # may trigger an error in ancestor's del_entity_type method |
530 # may trigger an error in ancestor's del_entity_type method |
504 if 'has_text' in eschema.subject_relations(): |
531 if 'has_text' in eschema.subject_relations(): |
505 self.del_relation_def(etype, 'has_text', 'String') |
532 self.del_relation_def(etype, 'has_text', 'String') |
506 super(CubicWebSchema, self).del_entity_type(etype) |
533 super(CubicWebSchema, self).del_entity_type(etype) |
507 |
534 |
508 def schema_by_eid(self, eid): |
535 def schema_by_eid(self, eid): |
509 return self._eid_index[eid] |
536 return self._eid_index[eid] |
510 |
537 |
511 |
538 |
512 # Possible constraints ######################################################## |
539 # Possible constraints ######################################################## |
514 class RQLVocabularyConstraint(BaseConstraint): |
541 class RQLVocabularyConstraint(BaseConstraint): |
515 """the rql vocabulary constraint : |
542 """the rql vocabulary constraint : |
516 |
543 |
517 limit the proposed values to a set of entities returned by a rql query, |
544 limit the proposed values to a set of entities returned by a rql query, |
518 but this is not enforced at the repository level |
545 but this is not enforced at the repository level |
519 |
546 |
520 restriction is additional rql restriction that will be added to |
547 restriction is additional rql restriction that will be added to |
521 a predefined query, where the S and O variables respectivly represent |
548 a predefined query, where the S and O variables respectivly represent |
522 the subject and the object of the relation |
549 the subject and the object of the relation |
523 """ |
550 """ |
524 |
551 |
525 def __init__(self, restriction): |
552 def __init__(self, restriction): |
526 self.restriction = restriction |
553 self.restriction = restriction |
527 |
554 |
528 def serialize(self): |
555 def serialize(self): |
529 return self.restriction |
556 return self.restriction |
530 |
557 |
531 def deserialize(cls, value): |
558 def deserialize(cls, value): |
532 return cls(value) |
559 return cls(value) |
533 deserialize = classmethod(deserialize) |
560 deserialize = classmethod(deserialize) |
534 |
561 |
535 def check(self, entity, rtype, value): |
562 def check(self, entity, rtype, value): |
536 """return true if the value satisfy the constraint, else false""" |
563 """return true if the value satisfy the constraint, else false""" |
537 # implemented as a hook in the repository |
564 # implemented as a hook in the repository |
538 return 1 |
565 return 1 |
539 |
566 |
540 def repo_check(self, session, eidfrom, rtype, eidto): |
567 def repo_check(self, session, eidfrom, rtype, eidto): |
541 """raise ValidationError if the relation doesn't satisfy the constraint |
568 """raise ValidationError if the relation doesn't satisfy the constraint |
542 """ |
569 """ |
543 pass # this is a vocabulary constraint, not enforce |
570 pass # this is a vocabulary constraint, not enforce |
544 |
571 |
545 def __str__(self): |
572 def __str__(self): |
546 return self.restriction |
573 return self.restriction |
547 |
574 |
548 def __repr__(self): |
575 def __repr__(self): |
549 return '<%s : %s>' % (self.__class__.__name__, repr(self.restriction)) |
576 return '<%s : %s>' % (self.__class__.__name__, repr(self.restriction)) |
557 rql = 'Any S,O WHERE S eid %(s)s, O eid %(o)s, ' + self.restriction |
584 rql = 'Any S,O WHERE S eid %(s)s, O eid %(o)s, ' + self.restriction |
558 return session.unsafe_execute(rql, {'s': eidfrom, 'o': eidto}, |
585 return session.unsafe_execute(rql, {'s': eidfrom, 'o': eidto}, |
559 ('s', 'o'), build_descr=False) |
586 ('s', 'o'), build_descr=False) |
560 def error(self, eid, rtype, msg): |
587 def error(self, eid, rtype, msg): |
561 raise ValidationError(eid, {rtype: msg}) |
588 raise ValidationError(eid, {rtype: msg}) |
562 |
589 |
563 def repo_check(self, session, eidfrom, rtype, eidto): |
590 def repo_check(self, session, eidfrom, rtype, eidto): |
564 """raise ValidationError if the relation doesn't satisfy the constraint |
591 """raise ValidationError if the relation doesn't satisfy the constraint |
565 """ |
592 """ |
566 if not self.exec_query(session, eidfrom, eidto): |
593 if not self.exec_query(session, eidfrom, eidto): |
567 # XXX at this point dunno if the validation error `occured` on |
594 # XXX at this point dunno if the validation error `occured` on |
579 if len(self.exec_query(session, eidfrom, eidto)) > 1: |
606 if len(self.exec_query(session, eidfrom, eidto)) > 1: |
580 # XXX at this point dunno if the validation error `occured` on |
607 # XXX at this point dunno if the validation error `occured` on |
581 # eidfrom or eidto (from user interface point of view) |
608 # eidfrom or eidto (from user interface point of view) |
582 self.error(eidfrom, rtype, 'unique constraint %s failed' % self) |
609 self.error(eidfrom, rtype, 'unique constraint %s failed' % self) |
583 |
610 |
584 |
611 |
585 def split_expression(rqlstring): |
612 def split_expression(rqlstring): |
586 for expr in rqlstring.split(','): |
613 for expr in rqlstring.split(','): |
587 for word in expr.split(): |
614 for word in expr.split(): |
588 yield word |
615 yield word |
589 |
616 |
590 def normalize_expression(rqlstring): |
617 def normalize_expression(rqlstring): |
591 """normalize an rql expression to ease schema synchronization (avoid |
618 """normalize an rql expression to ease schema synchronization (avoid |
592 suppressing and reinserting an expression if only a space has been added/removed |
619 suppressing and reinserting an expression if only a space has been added/removed |
593 for instance) |
620 for instance) |
594 """ |
621 """ |
608 raise RQLSyntaxError(expression) |
635 raise RQLSyntaxError(expression) |
609 for mainvar in mainvars.split(','): |
636 for mainvar in mainvars.split(','): |
610 if len(self.rqlst.defined_vars[mainvar].references()) <= 2: |
637 if len(self.rqlst.defined_vars[mainvar].references()) <= 2: |
611 LOGGER.warn('You did not use the %s variable in your RQL expression %s', |
638 LOGGER.warn('You did not use the %s variable in your RQL expression %s', |
612 mainvar, self) |
639 mainvar, self) |
613 |
640 |
614 def __str__(self): |
641 def __str__(self): |
615 return self.full_rql |
642 return self.full_rql |
616 def __repr__(self): |
643 def __repr__(self): |
617 return '%s(%s)' % (self.__class__.__name__, self.full_rql) |
644 return '%s(%s)' % (self.__class__.__name__, self.full_rql) |
618 |
645 |
619 def __deepcopy__(self, memo): |
646 def __deepcopy__(self, memo): |
620 return self.__class__(self.expression, self.mainvars) |
647 return self.__class__(self.expression, self.mainvars) |
621 def __getstate__(self): |
648 def __getstate__(self): |
622 return (self.expression, self.mainvars) |
649 return (self.expression, self.mainvars) |
623 def __setstate__(self, state): |
650 def __setstate__(self, state): |
624 self.__init__(*state) |
651 self.__init__(*state) |
625 |
652 |
626 @cached |
653 @cached |
627 def transform_has_permission(self): |
654 def transform_has_permission(self): |
628 found = None |
655 found = None |
629 rqlst = self.rqlst |
656 rqlst = self.rqlst |
630 for var in rqlst.defined_vars.itervalues(): |
657 for var in rqlst.defined_vars.itervalues(): |
749 if 'X' in defined: |
776 if 'X' in defined: |
750 rql += ', X eid %(x)s' |
777 rql += ', X eid %(x)s' |
751 if 'U' in defined: |
778 if 'U' in defined: |
752 rql += ', U eid %(u)s' |
779 rql += ', U eid %(u)s' |
753 return rql |
780 return rql |
754 |
781 |
755 def check(self, session, eid=None): |
782 def check(self, session, eid=None): |
756 if 'X' in self.rqlst.defined_vars: |
783 if 'X' in self.rqlst.defined_vars: |
757 if eid is None: |
784 if eid is None: |
758 return False |
785 return False |
759 return self._check(session, x=eid) |
786 return self._check(session, x=eid) |
760 return self._check(session) |
787 return self._check(session) |
761 |
788 |
762 PyFileReader.context['ERQLExpression'] = ERQLExpression |
789 PyFileReader.context['ERQLExpression'] = ERQLExpression |
763 |
790 |
764 class RRQLExpression(RQLExpression): |
791 class RRQLExpression(RQLExpression): |
765 def __init__(self, expression, mainvars=None, eid=None): |
792 def __init__(self, expression, mainvars=None, eid=None): |
766 if mainvars is None: |
793 if mainvars is None: |
767 defined = set(split_expression(expression)) |
794 defined = set(split_expression(expression)) |
768 mainvars = [] |
795 mainvars = [] |
800 if 'O' in self.rqlst.defined_vars: |
827 if 'O' in self.rqlst.defined_vars: |
801 if toeid is None: |
828 if toeid is None: |
802 return False |
829 return False |
803 kwargs['o'] = toeid |
830 kwargs['o'] = toeid |
804 return self._check(session, **kwargs) |
831 return self._check(session, **kwargs) |
805 |
832 |
806 PyFileReader.context['RRQLExpression'] = RRQLExpression |
833 PyFileReader.context['RRQLExpression'] = RRQLExpression |
807 |
834 |
808 |
835 # workflow extensions ######################################################### |
|
836 |
|
837 class workflowable_definition(ybo.metadefinition): |
|
838 """extends default EntityType's metaclass to add workflow relations |
|
839 (i.e. in_state and wf_info_for). |
|
840 This is the default metaclass for WorkflowableEntityType |
|
841 """ |
|
842 def __new__(mcs, name, bases, classdict): |
|
843 abstract = classdict.pop('abstract', False) |
|
844 defclass = super(workflowable_definition, mcs).__new__(mcs, name, bases, classdict) |
|
845 if not abstract: |
|
846 existing_rels = set(rdef.name for rdef in defclass.__relations__) |
|
847 if 'in_state' not in existing_rels and 'wf_info_for' not in existing_rels: |
|
848 in_state = ybo.SubjectRelation('State', cardinality='1*', |
|
849 # XXX automatize this |
|
850 constraints=[RQLConstraint('S is ET, O state_of ET')], |
|
851 description=_('account state')) |
|
852 yams_add_relation(defclass.__relations__, in_state, 'in_state') |
|
853 wf_info_for = ybo.ObjectRelation('TrInfo', cardinality='1*', composite='object') |
|
854 yams_add_relation(defclass.__relations__, wf_info_for, 'wf_info_for') |
|
855 return defclass |
|
856 |
|
857 class WorkflowableEntityType(ybo.EntityType): |
|
858 __metaclass__ = workflowable_definition |
|
859 abstract = True |
|
860 |
|
861 PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType |
|
862 |
809 # schema loading ############################################################## |
863 # schema loading ############################################################## |
810 |
864 |
811 class CubicWebRelationFileReader(RelationFileReader): |
865 class CubicWebRelationFileReader(RelationFileReader): |
812 """cubicweb specific relation file reader, handling additional RQL |
866 """cubicweb specific relation file reader, handling additional RQL |
813 constraints on a relation definition |
867 constraints on a relation definition |
814 """ |
868 """ |
815 |
869 |
816 def handle_constraint(self, rdef, constraint_text): |
870 def handle_constraint(self, rdef, constraint_text): |
817 """arbitrary constraint is an rql expression for cubicweb""" |
871 """arbitrary constraint is an rql expression for cubicweb""" |
818 if not rdef.constraints: |
872 if not rdef.constraints: |
819 rdef.constraints = [] |
873 rdef.constraints = [] |
820 rdef.constraints.append(RQLVocabularyConstraint(constraint_text)) |
874 rdef.constraints.append(RQLVocabularyConstraint(constraint_text)) |
822 def process_properties(self, rdef, relation_def): |
876 def process_properties(self, rdef, relation_def): |
823 if 'inline' in relation_def: |
877 if 'inline' in relation_def: |
824 rdef.inlined = True |
878 rdef.inlined = True |
825 RelationFileReader.process_properties(self, rdef, relation_def) |
879 RelationFileReader.process_properties(self, rdef, relation_def) |
826 |
880 |
827 |
881 |
828 CONSTRAINTS['RQLConstraint'] = RQLConstraint |
882 CONSTRAINTS['RQLConstraint'] = RQLConstraint |
829 CONSTRAINTS['RQLUniqueConstraint'] = RQLUniqueConstraint |
883 CONSTRAINTS['RQLUniqueConstraint'] = RQLUniqueConstraint |
830 CONSTRAINTS['RQLVocabularyConstraint'] = RQLVocabularyConstraint |
884 CONSTRAINTS['RQLVocabularyConstraint'] = RQLVocabularyConstraint |
831 PyFileReader.context.update(CONSTRAINTS) |
885 PyFileReader.context.update(CONSTRAINTS) |
832 |
886 |
837 """ |
891 """ |
838 schemacls = CubicWebSchema |
892 schemacls = CubicWebSchema |
839 SchemaLoader.file_handlers.update({'.rel' : CubicWebRelationFileReader, |
893 SchemaLoader.file_handlers.update({'.rel' : CubicWebRelationFileReader, |
840 }) |
894 }) |
841 |
895 |
842 def load(self, config, path=()): |
896 def load(self, config, path=(), **kwargs): |
843 """return a Schema instance from the schema definition read |
897 """return a Schema instance from the schema definition read |
844 from <directory> |
898 from <directory> |
845 """ |
899 """ |
846 self.lib_directory = config.schemas_lib_dir() |
900 self.lib_directory = config.schemas_lib_dir() |
847 return super(BootstrapSchemaLoader, self).load( |
901 return super(BootstrapSchemaLoader, self).load( |
848 path, config.appid, register_base_types=False) |
902 path, config.appid, register_base_types=False, **kwargs) |
849 |
903 |
850 def _load_definition_files(self, cubes=None): |
904 def _load_definition_files(self, cubes=None): |
851 # bootstraping, ignore cubes |
905 # bootstraping, ignore cubes |
852 for filepath in self.include_schema_files('bootstrap'): |
906 for filepath in self.include_schema_files('bootstrap'): |
853 self.info('loading %s', filepath) |
907 self.info('loading %s', filepath) |
854 self.handle_file(filepath) |
908 self.handle_file(filepath) |
855 |
909 |
856 def unhandled_file(self, filepath): |
910 def unhandled_file(self, filepath): |
857 """called when a file without handler associated has been found""" |
911 """called when a file without handler associated has been found""" |
858 self.warning('ignoring file %r', filepath) |
912 self.warning('ignoring file %r', filepath) |
859 |
913 |
860 |
914 |
861 class CubicWebSchemaLoader(BootstrapSchemaLoader): |
915 class CubicWebSchemaLoader(BootstrapSchemaLoader): |
862 """cubicweb specific schema loader, automatically adding metadata to the |
916 """cubicweb specific schema loader, automatically adding metadata to the |
863 application's schema |
917 application's schema |
864 """ |
918 """ |
865 |
919 |
866 def load(self, config): |
920 def load(self, config, **kwargs): |
867 """return a Schema instance from the schema definition read |
921 """return a Schema instance from the schema definition read |
868 from <directory> |
922 from <directory> |
869 """ |
923 """ |
870 self.info('loading %s schemas', ', '.join(config.cubes())) |
924 self.info('loading %s schemas', ', '.join(config.cubes())) |
871 if config.apphome: |
925 if config.apphome: |
872 path = reversed([config.apphome] + config.cubes_path()) |
926 path = reversed([config.apphome] + config.cubes_path()) |
873 else: |
927 else: |
874 path = reversed(config.cubes_path()) |
928 path = reversed(config.cubes_path()) |
875 return super(CubicWebSchemaLoader, self).load(config, path=path) |
929 return super(CubicWebSchemaLoader, self).load(config, path=path, **kwargs) |
876 |
930 |
877 def _load_definition_files(self, cubes): |
931 def _load_definition_files(self, cubes): |
878 for filepath in (self.include_schema_files('bootstrap') |
932 for filepath in (self.include_schema_files('bootstrap') |
879 + self.include_schema_files('base') |
933 + self.include_schema_files('base') |
|
934 + self.include_schema_files('workflow') |
880 + self.include_schema_files('Bookmark')): |
935 + self.include_schema_files('Bookmark')): |
881 # + self.include_schema_files('Card')): |
|
882 self.info('loading %s', filepath) |
936 self.info('loading %s', filepath) |
883 self.handle_file(filepath) |
937 self.handle_file(filepath) |
884 for cube in cubes: |
938 for cube in cubes: |
885 for filepath in self.get_schema_files(cube): |
939 for filepath in self.get_schema_files(cube): |
886 self.info('loading %s', filepath) |
940 self.info('loading %s', filepath) |
890 # _() is just there to add messages to the catalog, don't care about actual |
944 # _() is just there to add messages to the catalog, don't care about actual |
891 # translation |
945 # translation |
892 PERM_USE_TEMPLATE_FORMAT = _('use_template_format') |
946 PERM_USE_TEMPLATE_FORMAT = _('use_template_format') |
893 |
947 |
894 class FormatConstraint(StaticVocabularyConstraint): |
948 class FormatConstraint(StaticVocabularyConstraint): |
895 need_perm_formats = (_('text/cubicweb-page-template'), |
949 need_perm_formats = [_('text/cubicweb-page-template')] |
896 ) |
950 |
897 regular_formats = (_('text/rest'), |
951 regular_formats = (_('text/rest'), |
898 _('text/html'), |
952 _('text/html'), |
899 _('text/plain'), |
953 _('text/plain'), |
900 ) |
954 ) |
901 def __init__(self): |
955 def __init__(self): |
902 pass |
956 pass |
|
957 |
903 def serialize(self): |
958 def serialize(self): |
904 """called to make persistent valuable data of a constraint""" |
959 """called to make persistent valuable data of a constraint""" |
905 return None |
960 return None |
906 |
961 |
907 @classmethod |
962 @classmethod |
908 def deserialize(cls, value): |
963 def deserialize(cls, value): |
909 """called to restore serialized data of a constraint. Should return |
964 """called to restore serialized data of a constraint. Should return |
910 a `cls` instance |
965 a `cls` instance |
911 """ |
966 """ |
912 return cls() |
967 return cls() |
913 |
968 |
914 def vocabulary(self, entity=None): |
969 def vocabulary(self, entity=None, req=None): |
915 if entity and entity.req.user.has_permission(PERM_USE_TEMPLATE_FORMAT): |
970 if req is None and entity is not None: |
916 return self.regular_formats + self.need_perm_formats |
971 req = entity.req |
|
972 if req is not None and req.user.has_permission(PERM_USE_TEMPLATE_FORMAT): |
|
973 return self.regular_formats + tuple(self.need_perm_formats) |
917 return self.regular_formats |
974 return self.regular_formats |
918 |
975 |
919 def __str__(self): |
976 def __str__(self): |
920 return 'value in (%s)' % u', '.join(repr(unicode(word)) for word in self.vocabulary()) |
977 return 'value in (%s)' % u', '.join(repr(unicode(word)) for word in self.vocabulary()) |
921 |
978 |
922 |
979 |
923 format_constraint = FormatConstraint() |
980 format_constraint = FormatConstraint() |
924 CONSTRAINTS['FormatConstraint'] = FormatConstraint |
981 CONSTRAINTS['FormatConstraint'] = FormatConstraint |
925 PyFileReader.context['format_constraint'] = format_constraint |
982 PyFileReader.context['format_constraint'] = format_constraint |
926 |
983 |
927 from logging import getLogger |
|
928 from cubicweb import set_log_methods |
|
929 set_log_methods(CubicWebSchemaLoader, getLogger('cubicweb.schemaloader')) |
984 set_log_methods(CubicWebSchemaLoader, getLogger('cubicweb.schemaloader')) |
930 set_log_methods(BootstrapSchemaLoader, getLogger('cubicweb.bootstrapschemaloader')) |
985 set_log_methods(BootstrapSchemaLoader, getLogger('cubicweb.bootstrapschemaloader')) |
931 set_log_methods(RQLExpression, getLogger('cubicweb.schema')) |
986 set_log_methods(RQLExpression, getLogger('cubicweb.schema')) |
932 |
987 |
933 # XXX monkey patch PyFileReader.import_erschema until bw_normalize_etype is |
988 # XXX monkey patch PyFileReader.import_erschema until bw_normalize_etype is |
934 # necessary |
989 # necessary |
935 orig_import_erschema = PyFileReader.import_erschema |
990 orig_import_erschema = PyFileReader.import_erschema |
936 def bw_import_erschema(self, ertype, schemamod=None, instantiate=True): |
991 def bw_import_erschema(self, ertype, schemamod=None, instantiate=True): |
937 return orig_import_erschema(self, bw_normalize_etype(ertype), schemamod, instantiate) |
992 return orig_import_erschema(self, bw_normalize_etype(ertype), schemamod, instantiate) |
938 PyFileReader.import_erschema = bw_import_erschema |
993 PyFileReader.import_erschema = bw_import_erschema |
939 |
994 |
940 # XXX itou for some Statement methods |
995 # XXX itou for some Statement methods |
941 from rql import stmts |
996 from rql import stmts |
942 orig_get_etype = stmts.ScopeNode.get_etype |
997 orig_get_etype = stmts.ScopeNode.get_etype |
943 def bw_get_etype(self, name): |
998 def bw_get_etype(self, name): |
944 return orig_get_etype(self, bw_normalize_etype(name)) |
999 return orig_get_etype(self, bw_normalize_etype(name)) |