server/repository.py
changeset 5082 d6fd82a5a4e8
parent 5079 e646047f80cb
child 5084 d8f491cb046c
equal deleted inserted replaced
5052:c9dbd95333f7 5082:d6fd82a5a4e8
    36 
    36 
    37 from cubicweb import (CW_SOFTWARE_ROOT, CW_MIGRATION_MAP,
    37 from cubicweb import (CW_SOFTWARE_ROOT, CW_MIGRATION_MAP,
    38                       UnknownEid, AuthenticationError, ExecutionError,
    38                       UnknownEid, AuthenticationError, ExecutionError,
    39                       ETypeNotSupportedBySources, MultiSourcesError,
    39                       ETypeNotSupportedBySources, MultiSourcesError,
    40                       BadConnectionId, Unauthorized, ValidationError,
    40                       BadConnectionId, Unauthorized, ValidationError,
    41                       typed_eid)
    41                       typed_eid, onevent)
    42 from cubicweb import cwvreg, schema, server
    42 from cubicweb import cwvreg, schema, server
    43 from cubicweb.server import utils, hook, pool, querier, sources
    43 from cubicweb.server import utils, hook, pool, querier, sources
    44 from cubicweb.server.session import Session, InternalSession, security_enabled
    44 from cubicweb.server.session import Session, InternalSession, InternalManager, \
    45 
    45      security_enabled
    46 
       
    47 class CleanupEidTypeCacheOp(hook.SingleLastOperation):
       
    48     """on rollback of a insert query or commit of delete query, we have to
       
    49     clear repository's cache from no more valid entries
       
    50 
       
    51     NOTE: querier's rqlst/solutions cache may have been polluted too with
       
    52     queries such as Any X WHERE X eid 32 if 32 has been rollbacked however
       
    53     generated queries are unpredictable and analysing all the cache probably
       
    54     too expensive. Notice that there is no pb when using args to specify eids
       
    55     instead of giving them into the rql string.
       
    56     """
       
    57 
       
    58     def commit_event(self):
       
    59         """the observed connections pool has been rollbacked,
       
    60         remove inserted eid from repository type/source cache
       
    61         """
       
    62         try:
       
    63             self.session.repo.clear_caches(
       
    64                 self.session.transaction_data['pendingeids'])
       
    65         except KeyError:
       
    66             pass
       
    67 
       
    68     def rollback_event(self):
       
    69         """the observed connections pool has been rollbacked,
       
    70         remove inserted eid from repository type/source cache
       
    71         """
       
    72         try:
       
    73             self.session.repo.clear_caches(
       
    74                 self.session.transaction_data['neweids'])
       
    75         except KeyError:
       
    76             pass
       
    77 
    46 
    78 
    47 
    79 def del_existing_rel_if_needed(session, eidfrom, rtype, eidto):
    48 def del_existing_rel_if_needed(session, eidfrom, rtype, eidto):
    80     """delete existing relation when adding a new one if card is 1 or ?
    49     """delete existing relation when adding a new one if card is 1 or ?
    81 
    50 
   162         # cache (extid, source uri) -> eid
   131         # cache (extid, source uri) -> eid
   163         self._extid_cache = {}
   132         self._extid_cache = {}
   164         # open some connections pools
   133         # open some connections pools
   165         if config.open_connections_pools:
   134         if config.open_connections_pools:
   166             self.open_connections_pools()
   135             self.open_connections_pools()
       
   136         @onevent('after-registry-reload', self)
       
   137         def fix_user_classes(self):
       
   138             usercls = self.vreg['etypes'].etype_class('CWUser')
       
   139             for session in self._sessions.values():
       
   140                 if not isinstance(session.user, InternalManager):
       
   141                     session.user.__class__ = usercls
   167 
   142 
   168     def _bootstrap_hook_registry(self):
   143     def _bootstrap_hook_registry(self):
   169         """called during bootstrap since we need the metadata hooks"""
   144         """called during bootstrap since we need the metadata hooks"""
   170         hooksdirectory = join(CW_SOFTWARE_ROOT, 'hooks')
   145         hooksdirectory = join(CW_SOFTWARE_ROOT, 'hooks')
   171         self.vreg.init_registration([hooksdirectory])
   146         self.vreg.init_registration([hooksdirectory])
   396 
   371 
   397     def _login_from_email(self, login):
   372     def _login_from_email(self, login):
   398         session = self.internal_session()
   373         session = self.internal_session()
   399         try:
   374         try:
   400             rset = session.execute('Any L WHERE U login L, U primary_email M, '
   375             rset = session.execute('Any L WHERE U login L, U primary_email M, '
   401                                    'M address %(login)s', {'login': login})
   376                                    'M address %(login)s', {'login': login},
       
   377                                    build_descr=False)
   402             if rset.rowcount == 1:
   378             if rset.rowcount == 1:
   403                 login = rset[0][0]
   379                 login = rset[0][0]
   404         finally:
   380         finally:
   405             session.close()
   381             session.close()
   406         return login
   382         return login
   528         """
   504         """
   529         session = self.internal_session()
   505         session = self.internal_session()
   530         # for consistency, keep same error as unique check hook (although not required)
   506         # for consistency, keep same error as unique check hook (although not required)
   531         errmsg = session._('the value "%s" is already used, use another one')
   507         errmsg = session._('the value "%s" is already used, use another one')
   532         try:
   508         try:
   533             if (session.execute('CWUser X WHERE X login %(login)s', {'login': login})
   509             if (session.execute('CWUser X WHERE X login %(login)s', {'login': login},
       
   510                                 build_descr=False)
   534                 or session.execute('CWUser X WHERE X use_email C, C address %(login)s',
   511                 or session.execute('CWUser X WHERE X use_email C, C address %(login)s',
   535                                    {'login': login})):
   512                                    {'login': login}, build_descr=False)):
   536                 qname = role_name('login', 'subject')
   513                 qname = role_name('login', 'subject')
   537                 raise ValidationError(None, {qname: errmsg % login})
   514                 raise ValidationError(None, {qname: errmsg % login})
   538             # we have to create the user
   515             # we have to create the user
   539             user = self.vreg['etypes'].etype_class('CWUser')(session, None)
   516             user = self.vreg['etypes'].etype_class('CWUser')(session)
   540             if isinstance(password, unicode):
   517             if isinstance(password, unicode):
   541                 # password should *always* be utf8 encoded
   518                 # password should *always* be utf8 encoded
   542                 password = password.encode('UTF8')
   519                 password = password.encode('UTF8')
   543             kwargs['login'] = login
   520             kwargs['login'] = login
   544             kwargs['upassword'] = password
   521             kwargs['upassword'] = password
   546             self.glob_add_entity(session, user)
   523             self.glob_add_entity(session, user)
   547             session.execute('SET X in_group G WHERE X eid %(x)s, G name "users"',
   524             session.execute('SET X in_group G WHERE X eid %(x)s, G name "users"',
   548                             {'x': user.eid})
   525                             {'x': user.eid})
   549             if email or '@' in login:
   526             if email or '@' in login:
   550                 d = {'login': login, 'email': email or login}
   527                 d = {'login': login, 'email': email or login}
   551                 if session.execute('EmailAddress X WHERE X address %(email)s', d):
   528                 if session.execute('EmailAddress X WHERE X address %(email)s', d,
       
   529                                    build_descr=False):
   552                     qname = role_name('address', 'subject')
   530                     qname = role_name('address', 'subject')
   553                     raise ValidationError(None, {qname: errmsg % d['email']})
   531                     raise ValidationError(None, {qname: errmsg % d['email']})
   554                 session.execute('INSERT EmailAddress X: X address %(email)s, '
   532                 session.execute('INSERT EmailAddress X: X address %(email)s, '
   555                                 'U primary_email X, U use_email X '
   533                                 'U primary_email X, U use_email X '
   556                                 'WHERE U login %(login)s', d)
   534                                 'WHERE U login %(login)s', d, build_descr=False)
   557             session.commit()
   535             session.commit()
   558         finally:
   536         finally:
   559             session.close()
   537             session.close()
   560         return True
   538         return True
   561 
   539 
   931     def add_info(self, session, entity, source, extid=None, complete=True):
   909     def add_info(self, session, entity, source, extid=None, complete=True):
   932         """add type and source info for an eid into the system table,
   910         """add type and source info for an eid into the system table,
   933         and index the entity with the full text index
   911         and index the entity with the full text index
   934         """
   912         """
   935         # begin by inserting eid/type/source/extid into the entities table
   913         # begin by inserting eid/type/source/extid into the entities table
   936         new = session.transaction_data.setdefault('neweids', set())
   914         hook.set_operation(session, 'neweids', entity.eid,
   937         new.add(entity.eid)
   915                            hook.CleanupNewEidsCacheOp)
   938         self.system_source.add_info(session, entity, source, extid, complete)
   916         self.system_source.add_info(session, entity, source, extid, complete)
   939         CleanupEidTypeCacheOp(session)
       
   940 
   917 
   941     def delete_info(self, session, entity, sourceuri, extid):
   918     def delete_info(self, session, entity, sourceuri, extid):
   942         """called by external source when some entity known by the system source
   919         """called by external source when some entity known by the system source
   943         has been deleted in the external source
   920         has been deleted in the external source
   944         """
   921         """
   945         self._prepare_delete_info(session, entity, sourceuri)
   922         # mark eid as being deleted in session info and setup cache update
       
   923         # operation
       
   924         hook.set_operation(session, 'pendingeids', entity.eid,
       
   925                            hook.CleanupDeletedEidsCacheOp)
   946         self._delete_info(session, entity, sourceuri, extid)
   926         self._delete_info(session, entity, sourceuri, extid)
   947 
       
   948     def _prepare_delete_info(self, session, entity, sourceuri):
       
   949         """prepare the repository for deletion of an entity:
       
   950         * update the fti
       
   951         * mark eid as being deleted in session info
       
   952         * setup cache update operation
       
   953         * if undoable, get back all entity's attributes and relation
       
   954         """
       
   955         eid = entity.eid
       
   956         self.system_source.fti_unindex_entity(session, eid)
       
   957         pending = session.transaction_data.setdefault('pendingeids', set())
       
   958         pending.add(eid)
       
   959         CleanupEidTypeCacheOp(session)
       
   960 
   927 
   961     def _delete_info(self, session, entity, sourceuri, extid):
   928     def _delete_info(self, session, entity, sourceuri, extid):
   962                      # attributes=None, relations=None):
   929                      # attributes=None, relations=None):
   963         """delete system information on deletion of an entity:
   930         """delete system information on deletion of an entity:
   964         * delete all remaining relations from/to this entity
   931         * delete all remaining relations from/to this entity
   975                 if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
   942                 if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
   976                     continue
   943                     continue
   977                 if role == 'subject':
   944                 if role == 'subject':
   978                     # don't skip inlined relation so they are regularly
   945                     # don't skip inlined relation so they are regularly
   979                     # deleted and so hooks are correctly called
   946                     # deleted and so hooks are correctly called
   980                     selection = 'X %s Y' % rtype
   947                     rql = 'DELETE X %s Y WHERE X eid %%(x)s' % rtype
   981                 else:
   948                 else:
   982                     selection = 'Y %s X' % rtype
   949                     rql = 'DELETE Y %s X WHERE X eid %%(x)s' % rtype
   983                 rql = 'DELETE %s WHERE X eid %%(x)s' % selection
       
   984                 session.execute(rql, {'x': eid}, 'x', build_descr=False)
   950                 session.execute(rql, {'x': eid}, 'x', build_descr=False)
   985         self.system_source.delete_info(session, entity, sourceuri, extid)
   951         self.system_source.delete_info(session, entity, sourceuri, extid)
   986 
   952 
   987     def locate_relation_source(self, session, subject, rtype, object):
   953     def locate_relation_source(self, session, subject, rtype, object):
   988         subjsource = self.source_from_eid(subject, session)
   954         subjsource = self.source_from_eid(subject, session)
  1009             if source.support_entity(etype, 1):
   975             if source.support_entity(etype, 1):
  1010                 return source
   976                 return source
  1011         else:
   977         else:
  1012             raise ETypeNotSupportedBySources(etype)
   978             raise ETypeNotSupportedBySources(etype)
  1013 
   979 
       
   980     def init_entity_caches(self, session, entity, source):
       
   981         """add entity to session entities cache and repo's extid cache.
       
   982         Return entity's ext id if the source isn't the system source.
       
   983         """
       
   984         session.set_entity_cache(entity)
       
   985         suri = source.uri
       
   986         if suri == 'system':
       
   987             extid = None
       
   988         else:
       
   989             extid = source.get_extid(entity)
       
   990             self._extid_cache[(str(extid), suri)] = entity.eid
       
   991         self._type_source_cache[entity.eid] = (entity.__regid__, suri, extid)
       
   992         return extid
       
   993 
  1014     def glob_add_entity(self, session, entity):
   994     def glob_add_entity(self, session, entity):
  1015         """add an entity to the repository
   995         """add an entity to the repository
  1016 
   996 
  1017         the entity eid should originaly be None and a unique eid is assigned to
   997         the entity eid should originaly be None and a unique eid is assigned to
  1018         the entity instance
   998         the entity instance
  1024         # XXX kill that transmutation feature !
  1004         # XXX kill that transmutation feature !
  1025         if not entity_ is entity:
  1005         if not entity_ is entity:
  1026             entity.__class__ = entity_.__class__
  1006             entity.__class__ = entity_.__class__
  1027             entity.__dict__.update(entity_.__dict__)
  1007             entity.__dict__.update(entity_.__dict__)
  1028         eschema = entity.e_schema
  1008         eschema = entity.e_schema
  1029         etype = str(eschema)
  1009         source = self.locate_etype_source(entity.__regid__)
  1030         source = self.locate_etype_source(etype)
  1010         # allocate an eid to the entity before calling hooks
  1031         # attribute an eid to the entity before calling hooks
       
  1032         entity.set_eid(self.system_source.create_eid(session))
  1011         entity.set_eid(self.system_source.create_eid(session))
       
  1012         # set caches asap
       
  1013         extid = self.init_entity_caches(session, entity, source)
  1033         if server.DEBUG & server.DBG_REPO:
  1014         if server.DEBUG & server.DBG_REPO:
  1034             print 'ADD entity', etype, entity.eid, dict(entity)
  1015             print 'ADD entity', entity.__regid__, entity.eid, dict(entity)
  1035         relations = []
  1016         relations = []
  1036         if source.should_call_hooks:
  1017         if source.should_call_hooks:
  1037             self.hm.call_hooks('before_add_entity', session, entity=entity)
  1018             self.hm.call_hooks('before_add_entity', session, entity=entity)
  1038         # XXX use entity.keys here since edited_attributes is not updated for
  1019         # XXX use entity.keys here since edited_attributes is not updated for
  1039         # inline relations
  1020         # inline relations XXX not true, right? (see edited_attributes
       
  1021         # affectation above)
  1040         for attr in entity.iterkeys():
  1022         for attr in entity.iterkeys():
  1041             rschema = eschema.subjrels[attr]
  1023             rschema = eschema.subjrels[attr]
  1042             if not rschema.final: # inlined relation
  1024             if not rschema.final: # inlined relation
  1043                 relations.append((attr, entity[attr]))
  1025                 relations.append((attr, entity[attr]))
  1044         entity.set_defaults()
  1026         entity.set_defaults()
  1045         if session.is_hook_category_activated('integrity'):
  1027         if session.is_hook_category_activated('integrity'):
  1046             entity.check(creation=True)
  1028             entity.check(creation=True)
  1047         source.add_entity(session, entity)
  1029         source.add_entity(session, entity)
  1048         if source.uri != 'system':
       
  1049             extid = source.get_extid(entity)
       
  1050             self._extid_cache[(str(extid), source.uri)] = entity.eid
       
  1051         else:
       
  1052             extid = None
       
  1053         self.add_info(session, entity, source, extid, complete=False)
  1030         self.add_info(session, entity, source, extid, complete=False)
  1054         entity._is_saved = True # entity has an eid and is saved
  1031         entity._is_saved = True # entity has an eid and is saved
  1055         # prefill entity relation caches
  1032         # prefill entity relation caches
  1056         session.set_entity_cache(entity)
       
  1057         for rschema in eschema.subject_relations():
  1033         for rschema in eschema.subject_relations():
  1058             rtype = str(rschema)
  1034             rtype = str(rschema)
  1059             if rtype in schema.VIRTUAL_RTYPES:
  1035             if rtype in schema.VIRTUAL_RTYPES:
  1060                 continue
  1036                 continue
  1061             if rschema.final:
  1037             if rschema.final:
  1083 
  1059 
  1084     def glob_update_entity(self, session, entity, edited_attributes):
  1060     def glob_update_entity(self, session, entity, edited_attributes):
  1085         """replace an entity in the repository
  1061         """replace an entity in the repository
  1086         the type and the eid of an entity must not be changed
  1062         the type and the eid of an entity must not be changed
  1087         """
  1063         """
  1088         etype = str(entity.e_schema)
       
  1089         if server.DEBUG & server.DBG_REPO:
  1064         if server.DEBUG & server.DBG_REPO:
  1090             print 'UPDATE entity', etype, entity.eid, \
  1065             print 'UPDATE entity', entity.__regid__, entity.eid, \
  1091                   dict(entity), edited_attributes
  1066                   dict(entity), edited_attributes
  1092         entity.edited_attributes = edited_attributes
  1067         entity.edited_attributes = edited_attributes
  1093         if session.is_hook_category_activated('integrity'):
  1068         if session.is_hook_category_activated('integrity'):
  1094             entity.check()
  1069             entity.check()
  1095         eschema = entity.e_schema
  1070         eschema = entity.e_schema
  1148 
  1123 
  1149     def glob_delete_entity(self, session, eid):
  1124     def glob_delete_entity(self, session, eid):
  1150         """delete an entity and all related entities from the repository"""
  1125         """delete an entity and all related entities from the repository"""
  1151         entity = session.entity_from_eid(eid)
  1126         entity = session.entity_from_eid(eid)
  1152         etype, sourceuri, extid = self.type_and_source_from_eid(eid, session)
  1127         etype, sourceuri, extid = self.type_and_source_from_eid(eid, session)
  1153         self._prepare_delete_info(session, entity, sourceuri)
       
  1154         if server.DEBUG & server.DBG_REPO:
  1128         if server.DEBUG & server.DBG_REPO:
  1155             print 'DELETE entity', etype, eid
  1129             print 'DELETE entity', etype, eid
  1156         source = self.sources_by_uri[sourceuri]
  1130         source = self.sources_by_uri[sourceuri]
  1157         if source.should_call_hooks:
  1131         if source.should_call_hooks:
  1158             self.hm.call_hooks('before_delete_entity', session, entity=entity)
  1132             self.hm.call_hooks('before_delete_entity', session, entity=entity)