1 # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
3 # |
3 # |
4 # This file is part of CubicWeb. |
4 # This file is part of CubicWeb. |
5 # |
5 # |
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
28 from copy import copy |
28 from copy import copy |
29 from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema |
29 from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema |
30 from yams import buildobjs as ybo, schema2sql as y2sql |
30 from yams import buildobjs as ybo, schema2sql as y2sql |
31 |
31 |
32 from logilab.common.decorators import clear_cache |
32 from logilab.common.decorators import clear_cache |
33 from logilab.common.testlib import mock_object |
|
34 |
33 |
35 from cubicweb import ValidationError |
34 from cubicweb import ValidationError |
36 from cubicweb.selectors import is_instance |
35 from cubicweb.selectors import is_instance |
37 from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES, |
36 from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES, |
38 CONSTRAINTS, ETYPE_NAME_MAP, display_name) |
37 CONSTRAINTS, ETYPE_NAME_MAP, display_name) |
119 |
118 |
120 |
119 |
121 def check_valid_changes(session, entity, ro_attrs=('name', 'final')): |
120 def check_valid_changes(session, entity, ro_attrs=('name', 'final')): |
122 errors = {} |
121 errors = {} |
123 # don't use getattr(entity, attr), we would get the modified value if any |
122 # don't use getattr(entity, attr), we would get the modified value if any |
124 for attr in entity.edited_attributes: |
123 for attr in entity.cw_edited: |
125 if attr in ro_attrs: |
124 if attr in ro_attrs: |
126 newval = entity.pop(attr) |
125 origval, newval = entity.cw_edited.oldnewvalue(attr) |
127 origval = getattr(entity, attr) |
|
128 if newval != origval: |
126 if newval != origval: |
129 errors[attr] = session._("can't change the %s attribute") % \ |
127 errors[attr] = session._("can't change the %s attribute") % \ |
130 display_name(session, attr) |
128 display_name(session, attr) |
131 entity[attr] = newval |
|
132 if errors: |
129 if errors: |
133 raise ValidationError(entity.eid, errors) |
130 raise ValidationError(entity.eid, errors) |
|
131 |
|
132 |
|
133 class _MockEntity(object): # XXX use a named tuple with python 2.6 |
|
134 def __init__(self, eid): |
|
135 self.eid = eid |
134 |
136 |
135 |
137 |
136 class SyncSchemaHook(hook.Hook): |
138 class SyncSchemaHook(hook.Hook): |
137 """abstract class for schema synchronization hooks (in the `syncschema` |
139 """abstract class for schema synchronization hooks (in the `syncschema` |
138 category) |
140 category) |
257 session.system_sql(sql) |
259 session.system_sql(sql) |
258 # add meta relations |
260 # add meta relations |
259 gmap = group_mapping(session) |
261 gmap = group_mapping(session) |
260 cmap = ss.cstrtype_mapping(session) |
262 cmap = ss.cstrtype_mapping(session) |
261 for rtype in (META_RTYPES - VIRTUAL_RTYPES): |
263 for rtype in (META_RTYPES - VIRTUAL_RTYPES): |
262 rschema = schema[rtype] |
264 try: |
|
265 rschema = schema[rtype] |
|
266 except: |
|
267 if rtype == 'cw_source': |
|
268 continue # XXX 3.10 migration |
|
269 raise |
263 sampletype = rschema.subjects()[0] |
270 sampletype = rschema.subjects()[0] |
264 desttype = rschema.objects()[0] |
271 desttype = rschema.objects()[0] |
265 rdef = copy(rschema.rdef(sampletype, desttype)) |
272 rdef = copy(rschema.rdef(sampletype, desttype)) |
266 rdef.subject = mock_object(eid=entity.eid) |
273 rdef.subject = _MockEntity(eid=entity.eid) |
267 mock = mock_object(eid=None) |
274 mock = _MockEntity(eid=None) |
268 ss.execschemarql(session.execute, mock, ss.rdef2rql(rdef, cmap, gmap)) |
275 ss.execschemarql(session.execute, mock, ss.rdef2rql(rdef, cmap, gmap)) |
269 |
276 |
270 def revertprecommit_event(self): |
277 def revertprecommit_event(self): |
271 # revert changes on in memory schema |
278 # revert changes on in memory schema |
272 self.session.vreg.schema.del_entity_type(self.entity.name) |
279 self.session.vreg.schema.del_entity_type(self.entity.name) |
309 rschema = self.rschema |
316 rschema = self.rschema |
310 if rschema.final: |
317 if rschema.final: |
311 return # watched changes to final relation type are unexpected |
318 return # watched changes to final relation type are unexpected |
312 session = self.session |
319 session = self.session |
313 if 'fulltext_container' in self.values: |
320 if 'fulltext_container' in self.values: |
|
321 op = UpdateFTIndexOp.get_instance(session) |
314 for subjtype, objtype in rschema.rdefs: |
322 for subjtype, objtype in rschema.rdefs: |
315 hook.set_operation(session, 'fti_update_etypes', subjtype, |
323 op.add_data(subjtype) |
316 UpdateFTIndexOp) |
324 op.add_data(objtype) |
317 hook.set_operation(session, 'fti_update_etypes', objtype, |
|
318 UpdateFTIndexOp) |
|
319 # update the in-memory schema first |
325 # update the in-memory schema first |
320 self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values) |
326 self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values) |
321 self.rschema.__dict__.update(self.values) |
327 self.rschema.__dict__.update(self.values) |
322 # then make necessary changes to the system source database |
328 # then make necessary changes to the system source database |
323 if not 'inlined' in self.values: |
329 if not 'inlined' in self.values: |
605 rdef.rtype.inlined) \ |
611 rdef.rtype.inlined) \ |
606 and self.values['cardinality'][0] != self.oldvalues['cardinality'][0]: |
612 and self.values['cardinality'][0] != self.oldvalues['cardinality'][0]: |
607 syssource.update_rdef_null_allowed(self.session, rdef) |
613 syssource.update_rdef_null_allowed(self.session, rdef) |
608 self.null_allowed_changed = True |
614 self.null_allowed_changed = True |
609 if 'fulltextindexed' in self.values: |
615 if 'fulltextindexed' in self.values: |
610 hook.set_operation(session, 'fti_update_etypes', rdef.subject, |
616 UpdateFTIndexOp.get_instance(session).add_data(rdef.subject) |
611 UpdateFTIndexOp) |
|
612 |
617 |
613 def revertprecommit_event(self): |
618 def revertprecommit_event(self): |
614 if self.rdef is None: |
619 if self.rdef is None: |
615 return |
620 return |
616 # revert changes on in memory schema |
621 # revert changes on in memory schema |
698 self.size_cstr_changed = True |
703 self.size_cstr_changed = True |
699 elif cstrtype == 'UniqueConstraint' and oldcstr is None: |
704 elif cstrtype == 'UniqueConstraint' and oldcstr is None: |
700 syssource.update_rdef_unique(session, rdef) |
705 syssource.update_rdef_unique(session, rdef) |
701 self.unique_changed = True |
706 self.unique_changed = True |
702 |
707 |
|
708 |
703 class CWUniqueTogetherConstraintAddOp(MemSchemaOperation): |
709 class CWUniqueTogetherConstraintAddOp(MemSchemaOperation): |
704 entity = None # make pylint happy |
710 entity = None # make pylint happy |
705 def precommit_event(self): |
711 def precommit_event(self): |
706 session = self.session |
712 session = self.session |
707 prefix = SQL_PREFIX |
713 prefix = SQL_PREFIX |
708 table = '%s%s' % (prefix, self.entity.constraint_of[0].name) |
714 table = '%s%s' % (prefix, self.entity.constraint_of[0].name) |
709 cols = ['%s%s' % (prefix, r.rtype.name) |
715 cols = ['%s%s' % (prefix, r.name) for r in self.entity.relations] |
710 for r in self.entity.relations] |
|
711 dbhelper= session.pool.source('system').dbhelper |
716 dbhelper= session.pool.source('system').dbhelper |
712 sqls = dbhelper.sqls_create_multicol_unique_index(table, cols) |
717 sqls = dbhelper.sqls_create_multicol_unique_index(table, cols) |
713 for sql in sqls: |
718 for sql in sqls: |
714 session.system_sql(sql) |
719 session.system_sql(sql) |
715 |
720 |
716 # XXX revertprecommit_event |
721 # XXX revertprecommit_event |
717 |
722 |
718 def postcommit_event(self): |
723 def postcommit_event(self): |
719 eschema = self.session.vreg.schema.schema_by_eid(self.entity.constraint_of[0].eid) |
724 eschema = self.session.vreg.schema.schema_by_eid(self.entity.constraint_of[0].eid) |
720 attrs = [r.rtype.name for r in self.entity.relations] |
725 attrs = [r.name for r in self.entity.relations] |
721 eschema._unique_together.append(attrs) |
726 eschema._unique_together.append(attrs) |
|
727 |
722 |
728 |
723 class CWUniqueTogetherConstraintDelOp(MemSchemaOperation): |
729 class CWUniqueTogetherConstraintDelOp(MemSchemaOperation): |
724 entity = oldcstr = None # for pylint |
730 entity = oldcstr = None # for pylint |
725 cols = [] # for pylint |
731 cols = [] # for pylint |
726 def precommit_event(self): |
732 def precommit_event(self): |
739 eschema = self.session.vreg.schema.schema_by_eid(self.entity.eid) |
745 eschema = self.session.vreg.schema.schema_by_eid(self.entity.eid) |
740 cols = set(self.cols) |
746 cols = set(self.cols) |
741 unique_together = [ut for ut in eschema._unique_together |
747 unique_together = [ut for ut in eschema._unique_together |
742 if set(ut) != cols] |
748 if set(ut) != cols] |
743 eschema._unique_together = unique_together |
749 eschema._unique_together = unique_together |
|
750 |
744 |
751 |
745 # operations for in-memory schema synchronization ############################# |
752 # operations for in-memory schema synchronization ############################# |
746 |
753 |
747 class MemSchemaCWETypeDel(MemSchemaOperation): |
754 class MemSchemaCWETypeDel(MemSchemaOperation): |
748 """actually remove the entity type from the instance's schema""" |
755 """actually remove the entity type from the instance's schema""" |
902 __regid__ = 'syncaddcwetype' |
909 __regid__ = 'syncaddcwetype' |
903 events = ('after_add_entity',) |
910 events = ('after_add_entity',) |
904 |
911 |
905 def __call__(self): |
912 def __call__(self): |
906 entity = self.entity |
913 entity = self.entity |
907 if entity.get('final'): |
914 if entity.cw_edited.get('final'): |
908 return |
915 return |
909 CWETypeAddOp(self._cw, entity=entity) |
916 CWETypeAddOp(self._cw, entity=entity) |
910 |
917 |
911 |
918 |
912 class BeforeUpdateCWETypeHook(DelCWETypeHook): |
919 class BeforeUpdateCWETypeHook(DelCWETypeHook): |
916 |
923 |
917 def __call__(self): |
924 def __call__(self): |
918 entity = self.entity |
925 entity = self.entity |
919 check_valid_changes(self._cw, entity, ro_attrs=('final',)) |
926 check_valid_changes(self._cw, entity, ro_attrs=('final',)) |
920 # don't use getattr(entity, attr), we would get the modified value if any |
927 # don't use getattr(entity, attr), we would get the modified value if any |
921 if 'name' in entity.edited_attributes: |
928 if 'name' in entity.cw_edited: |
922 oldname, newname = hook.entity_oldnewvalue(entity, 'name') |
929 oldname, newname = entity.cw_edited.oldnewvalue('name') |
923 if newname.lower() != oldname.lower(): |
930 if newname.lower() != oldname.lower(): |
924 CWETypeRenameOp(self._cw, oldname=oldname, newname=newname) |
931 CWETypeRenameOp(self._cw, oldname=oldname, newname=newname) |
925 |
932 |
926 |
933 |
927 # CWRType hooks ################################################################ |
934 # CWRType hooks ################################################################ |
960 |
967 |
961 def __call__(self): |
968 def __call__(self): |
962 entity = self.entity |
969 entity = self.entity |
963 rtypedef = ybo.RelationType(name=entity.name, |
970 rtypedef = ybo.RelationType(name=entity.name, |
964 description=entity.description, |
971 description=entity.description, |
965 inlined=entity.get('inlined', False), |
972 inlined=entity.cw_edited.get('inlined', False), |
966 symmetric=entity.get('symmetric', False), |
973 symmetric=entity.cw_edited.get('symmetric', False), |
967 eid=entity.eid) |
974 eid=entity.eid) |
968 MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef) |
975 MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef) |
969 |
976 |
970 |
977 |
971 class BeforeUpdateCWRTypeHook(DelCWRTypeHook): |
978 class BeforeUpdateCWRTypeHook(DelCWRTypeHook): |
976 def __call__(self): |
983 def __call__(self): |
977 entity = self.entity |
984 entity = self.entity |
978 check_valid_changes(self._cw, entity) |
985 check_valid_changes(self._cw, entity) |
979 newvalues = {} |
986 newvalues = {} |
980 for prop in ('symmetric', 'inlined', 'fulltext_container'): |
987 for prop in ('symmetric', 'inlined', 'fulltext_container'): |
981 if prop in entity.edited_attributes: |
988 if prop in entity.cw_edited: |
982 old, new = hook.entity_oldnewvalue(entity, prop) |
989 old, new = entity.cw_edited.oldnewvalue(prop) |
983 if old != new: |
990 if old != new: |
984 newvalues[prop] = entity[prop] |
991 newvalues[prop] = new |
985 if newvalues: |
992 if newvalues: |
986 rschema = self._cw.vreg.schema.rschema(entity.name) |
993 rschema = self._cw.vreg.schema.rschema(entity.name) |
987 CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity, |
994 CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity, |
988 values=newvalues) |
995 values=newvalues) |
989 |
996 |
1064 continue |
1071 continue |
1065 if prop == 'order': |
1072 if prop == 'order': |
1066 attr = 'ordernum' |
1073 attr = 'ordernum' |
1067 else: |
1074 else: |
1068 attr = prop |
1075 attr = prop |
1069 if attr in entity.edited_attributes: |
1076 if attr in entity.cw_edited: |
1070 old, new = hook.entity_oldnewvalue(entity, attr) |
1077 old, new = entity.cw_edited.oldnewvalue(attr) |
1071 if old != new: |
1078 if old != new: |
1072 newvalues[prop] = new |
1079 newvalues[prop] = new |
1073 if newvalues: |
1080 if newvalues: |
1074 RDefUpdateOp(self._cw, rschema=rschema, rdefkey=(subjtype, objtype), |
1081 RDefUpdateOp(self._cw, rschema=rschema, rdefkey=(subjtype, objtype), |
1075 values=newvalues) |
1082 values=newvalues) |
1118 # unique_together constraints |
1125 # unique_together constraints |
1119 # XXX: use setoperations and before_add_relation here (on constraint_of and relations) |
1126 # XXX: use setoperations and before_add_relation here (on constraint_of and relations) |
1120 class AfterAddCWUniqueTogetherConstraintHook(SyncSchemaHook): |
1127 class AfterAddCWUniqueTogetherConstraintHook(SyncSchemaHook): |
1121 __regid__ = 'syncadd_cwuniquetogether_constraint' |
1128 __regid__ = 'syncadd_cwuniquetogether_constraint' |
1122 __select__ = SyncSchemaHook.__select__ & is_instance('CWUniqueTogetherConstraint') |
1129 __select__ = SyncSchemaHook.__select__ & is_instance('CWUniqueTogetherConstraint') |
1123 events = ('after_add_entity', 'after_update_entity') |
1130 events = ('after_add_entity',) |
1124 |
1131 |
1125 def __call__(self): |
1132 def __call__(self): |
1126 CWUniqueTogetherConstraintAddOp(self._cw, entity=self.entity) |
1133 CWUniqueTogetherConstraintAddOp(self._cw, entity=self.entity) |
1127 |
1134 |
1128 |
1135 |
1135 if self._cw.deleted_in_transaction(self.eidto): |
1142 if self._cw.deleted_in_transaction(self.eidto): |
1136 return |
1143 return |
1137 schema = self._cw.vreg.schema |
1144 schema = self._cw.vreg.schema |
1138 cstr = self._cw.entity_from_eid(self.eidfrom) |
1145 cstr = self._cw.entity_from_eid(self.eidfrom) |
1139 entity = schema.schema_by_eid(self.eidto) |
1146 entity = schema.schema_by_eid(self.eidto) |
1140 cols = [r.rtype.name |
1147 cols = [r.name for r in cstr.relations] |
1141 for r in cstr.relations] |
1148 CWUniqueTogetherConstraintDelOp(self._cw, entity=entity, |
1142 CWUniqueTogetherConstraintDelOp(self._cw, entity=entity, oldcstr=cstr, cols=cols) |
1149 oldcstr=cstr, cols=cols) |
1143 |
1150 |
1144 |
1151 |
1145 # permissions synchronization hooks ############################################ |
1152 # permissions synchronization hooks ############################################ |
1146 |
1153 |
1147 class AfterAddPermissionHook(SyncSchemaHook): |
1154 class AfterAddPermissionHook(SyncSchemaHook): |
1183 MemSchemaPermissionDel(self._cw, action=action, eid=self.eidfrom, |
1190 MemSchemaPermissionDel(self._cw, action=action, eid=self.eidfrom, |
1184 expr=expr) |
1191 expr=expr) |
1185 |
1192 |
1186 |
1193 |
1187 |
1194 |
1188 class UpdateFTIndexOp(hook.SingleLastOperation): |
1195 class UpdateFTIndexOp(hook.DataOperationMixIn, hook.SingleLastOperation): |
1189 """operation to update full text indexation of entity whose schema change |
1196 """operation to update full text indexation of entity whose schema change |
1190 |
1197 |
1191 We wait after the commit to as the schema in memory is only updated after the commit. |
1198 We wait after the commit to as the schema in memory is only updated after |
|
1199 the commit. |
1192 """ |
1200 """ |
1193 |
1201 |
1194 def postcommit_event(self): |
1202 def postcommit_event(self): |
1195 session = self.session |
1203 session = self.session |
1196 source = session.repo.system_source |
1204 source = session.repo.system_source |
1197 to_reindex = session.transaction_data.pop('fti_update_etypes', ()) |
1205 schema = session.repo.vreg.schema |
|
1206 to_reindex = self.get_data() |
1198 self.info('%i etypes need full text indexed reindexation', |
1207 self.info('%i etypes need full text indexed reindexation', |
1199 len(to_reindex)) |
1208 len(to_reindex)) |
1200 schema = self.session.repo.vreg.schema |
|
1201 for etype in to_reindex: |
1209 for etype in to_reindex: |
1202 rset = session.execute('Any X WHERE X is %s' % etype) |
1210 rset = session.execute('Any X WHERE X is %s' % etype) |
1203 self.info('Reindexing full text index for %i entity of type %s', |
1211 self.info('Reindexing full text index for %i entity of type %s', |
1204 len(rset), etype) |
1212 len(rset), etype) |
1205 still_fti = list(schema[etype].indexable_attributes()) |
1213 still_fti = list(schema[etype].indexable_attributes()) |
1206 for entity in rset.entities(): |
1214 for entity in rset.entities(): |
1207 source.fti_unindex_entity(session, entity.eid) |
1215 source.fti_unindex_entities(session, [entity]) |
1208 for container in entity.cw_adapt_to('IFTIndexable').fti_containers(): |
1216 for container in entity.cw_adapt_to('IFTIndexable').fti_containers(): |
1209 if still_fti or container is not entity: |
1217 if still_fti or container is not entity: |
1210 source.fti_unindex_entity(session, container.eid) |
1218 source.fti_unindex_entities(session, [container]) |
1211 source.fti_index_entity(session, container) |
1219 source.fti_index_entities(session, [container]) |
1212 if len(to_reindex): |
1220 if to_reindex: |
1213 # Transaction have already been committed |
1221 # Transaction has already been committed |
1214 session.pool.commit() |
1222 session.pool.commit() |
1215 |
1223 |
1216 |
1224 |
1217 |
1225 |
1218 |
1226 |