goa/db.py
changeset 6366 1806148d6ce8
parent 6333 e3994fcc21c3
parent 6365 a15cc5e16178
child 6367 d4c485ec1ca1
equal deleted inserted replaced
6333:e3994fcc21c3 6366:1806148d6ce8
     1 # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """provide replacement classes for gae db module, so that a gae model can be
       
    19 used as base for a cubicweb application by simply replacing ::
       
    20 
       
    21   from google.appengine.ext import db
       
    22 
       
    23 by
       
    24 
       
    25   from cubicweb.goa import db
       
    26 
       
    27 The db.model api should be fully featured by replacement classes, with the
       
    28 following differences:
       
    29 
       
    30 * all methods returning `google.appengine.ext.db.Model` instance(s) will return
       
    31   `cubicweb.goa.db.Model` instance instead (though you should see almost no
       
    32   difference since those instances have the same api)
       
    33 
       
    34 * class methods returning model instance take a `req` as first argument, unless
       
    35   they are called through an instance, representing the current request
       
    36   (accessible through `self.req` on almost all objects)
       
    37 
       
    38 * XXX no instance.<modelname>_set attributes, use instance.reverse_<attr name>
       
    39       instead
       
    40 * XXX reference property always return a list of objects, not the instance
       
    41 * XXX name/collection_name argument of properties constructor are ignored
       
    42 * XXX ListProperty
       
    43 
       
    44 """
       
    45 __docformat__ = "restructuredtext en"
       
    46 
       
    47 from copy import deepcopy
       
    48 
       
    49 from logilab.common.decorators import cached, iclassmethod
       
    50 
       
    51 from cubicweb import Binary, entities
       
    52 from cubicweb.req import RequestSessionBase
       
    53 from cubicweb.rset import ResultSet
       
    54 from cubicweb.entity import metaentity
       
    55 from cubicweb.server.utils import crypt_password
       
    56 from cubicweb.goa import MODE
       
    57 from cubicweb.goa.dbinit import init_relations
       
    58 
       
    59 from google.appengine.api.datastore import Get, Put, Key, Entity, Query
       
    60 from google.appengine.api.datastore import NormalizeAndTypeCheck, RunInTransaction
       
    61 from google.appengine.api.datastore_types import Text, Blob
       
    62 from google.appengine.api.datastore_errors import BadKeyError
       
    63 
       
    64 # XXX remove this dependancy
       
    65 from google.appengine.ext import db
       
    66 
       
    67 
       
    68 def rset_from_objs(req, objs, attrs=('eid',), rql=None, args=None):
       
    69     """return a ResultSet instance for list of objects"""
       
    70     if objs is None:
       
    71         objs = ()
       
    72     elif isinstance(objs, Entity):
       
    73         objs = (objs,)
       
    74     if rql is None:
       
    75         rql = 'Any X'
       
    76     rows = []
       
    77     description = []
       
    78     rset = ResultSet(rows, rql, args, description=description)
       
    79     vreg = req.vreg
       
    80     for i, obj in enumerate(objs):
       
    81         line = []
       
    82         linedescr = []
       
    83         eschema = vreg.schema.eschema(obj.kind())
       
    84         for j, attr in enumerate(attrs):
       
    85             if attr == 'eid':
       
    86                 value = obj.key()
       
    87                 obj.row, obj.col = i, j
       
    88                 descr = eschema.type
       
    89                 value = str(value)
       
    90             else:
       
    91                 value = obj[attr]
       
    92                 descr = str(eschema.destination(attr))
       
    93             line.append(value)
       
    94             linedescr.append(descr)
       
    95         rows.append(line)
       
    96         description.append(linedescr)
       
    97         for j, attr in enumerate(attrs):
       
    98             if attr == 'eid':
       
    99                 entity = vreg.etype_class(eschema.type)(req, rset, i, j)
       
   100                 rset._get_entity_cache_ = {(i, j): entity}
       
   101     rset.rowcount = len(rows)
       
   102     rset.req = req
       
   103     return rset
       
   104 
       
   105 
       
   106 def needrequest(wrapped):
       
   107     def wrapper(cls, *args, **kwargs):
       
   108         req = kwargs.pop('req', None)
       
   109         if req is None and args and isinstance(args[0], RequestSessionBase):
       
   110             args = list(args)
       
   111             req = args.pop(0)
       
   112         if req is None:
       
   113             req = getattr(cls, 'req', None)
       
   114             if req is None:
       
   115                 raise Exception('either call this method on an instance or '
       
   116                                 'specify the req argument')
       
   117         return wrapped(cls, req, *args, **kwargs)
       
   118     return iclassmethod(wrapper)
       
   119 
       
   120 
       
   121 class gaedbmetaentity(metaentity):
       
   122     """metaclass for goa.db.Model classes: filter entity / db model part,
       
   123     put aside the db model part for later creation of db model class.
       
   124     """
       
   125     def __new__(mcs, name, bases, classdict):
       
   126         if not 'id' in classdict:
       
   127             classdict['id'] = name
       
   128         entitycls = super(gaedbmetaentity, mcs).__new__(mcs, name, bases, classdict)
       
   129         return entitycls
       
   130 
       
   131 
       
   132 TEST_MODELS = {}
       
   133 
       
   134 def extract_dbmodel(entitycls):
       
   135     if MODE == 'test' and entitycls in TEST_MODELS:
       
   136         dbclassdict = TEST_MODELS[entitycls]
       
   137     else:
       
   138         dbclassdict = {}
       
   139         for attr, value in entitycls.__dict__.items():
       
   140             if isinstance(value, db.Property) or isinstance(value, ReferencePropertyStub):
       
   141                 dbclassdict[attr] = value
       
   142                 # don't remove attr from entitycls, this make tests fail, and it's anyway
       
   143                 # overwritten by descriptor at class initialization time
       
   144                 #delattr(entitycls, attr)
       
   145     if MODE == 'test':
       
   146         TEST_MODELS[entitycls] = dbclassdict
       
   147         dbclassdict = deepcopy(dbclassdict)
       
   148         for propname, prop in TEST_MODELS[entitycls].iteritems():
       
   149             if getattr(prop, 'reference_class', None) is db._SELF_REFERENCE:
       
   150                 dbclassdict[propname].reference_class = db._SELF_REFERENCE
       
   151     return dbclassdict
       
   152 
       
   153 
       
   154 class Model(entities.AnyEntity):
       
   155     id = 'Any'
       
   156     __metaclass__ = gaedbmetaentity
       
   157 
       
   158     row = col = 0
       
   159 
       
   160     @classmethod
       
   161     def __initialize__(cls):
       
   162         super(Model, cls).__initialize__()
       
   163         cls._attributes = frozenset(rschema for rschema in cls.e_schema.subject_relations()
       
   164                                     if rschema.final)
       
   165 
       
   166     def __init__(self, *args, **kwargs):
       
   167         # db.Model prototype:
       
   168         #   __init__(self, parent=None, key_name=None, **kw)
       
   169         #
       
   170         # Entity prototype:
       
   171         #   __init__(self, req, rset, row=None, col=0)
       
   172         if args and isinstance(args[0], RequestSessionBase) or 'req' in kwargs:
       
   173             super(Model, self).__init__(*args, **kwargs)
       
   174             self._gaeinitargs = None
       
   175         else:
       
   176             super(Model, self).__init__(None, None)
       
   177             # if Model instances are given in kwargs, turn them into db model
       
   178             for key, val in kwargs.iteritems():
       
   179                 if key in self.e_schema.subject_relations() and not self.e_schema.schema[key].final:
       
   180                     if isinstance(kwargs, (list, tuple)):
       
   181                         val = [isinstance(x, Model) and x._dbmodel or x for x in val]
       
   182                     elif isinstance(val, Model):
       
   183                         val = val._dbmodel
       
   184                     kwargs[key] = val.key()
       
   185             self._gaeinitargs = (args, kwargs)
       
   186 
       
   187     def __repr__(self):
       
   188         return '<ModelEntity %s %s %s at %s>' % (
       
   189             self.e_schema, self.eid, self.keys(), id(self))
       
   190 
       
   191     def _cubicweb_to_datastore(self, attr, value):
       
   192         attr = attr[2:] # remove 's_' / 'o_' prefix
       
   193         if attr in self._attributes:
       
   194             tschema = self.e_schema.destination(attr)
       
   195             if tschema == 'String':
       
   196                 if len(value) > 500:
       
   197                     value = Text(value)
       
   198             elif tschema == 'Password':
       
   199                 # if value is a Binary instance, this mean we got it
       
   200                 # from a query result and so it is already encrypted
       
   201                 if isinstance(value, Binary):
       
   202                     value = value.getvalue()
       
   203                 else:
       
   204                     value = crypt_password(value)
       
   205             elif tschema == 'Bytes':
       
   206                 if isinstance(value, Binary):
       
   207                     value = value.getvalue()
       
   208                 value = Blob(value)
       
   209         else:
       
   210             value = Key(value)
       
   211         return value
       
   212 
       
   213     def _to_gae_dict(self, convert=True):
       
   214         gaedict = {}
       
   215         for attr, value in self.iteritems():
       
   216             attr = 's_' + attr
       
   217             if value is not None and convert:
       
   218                 value = self._cubicweb_to_datastore(attr, value)
       
   219             gaedict[attr] = value
       
   220         return gaedict
       
   221 
       
   222     def to_gae_model(self):
       
   223         dbmodel = self._dbmodel
       
   224         dbmodel.update(self._to_gae_dict())
       
   225         return dbmodel
       
   226 
       
   227     @property
       
   228     @cached
       
   229     def _dbmodel(self):
       
   230         if self.has_eid():
       
   231             assert self._gaeinitargs is None
       
   232             try:
       
   233                 return self.req.datastore_get(self.eid)
       
   234             except AttributeError: # self.req is not a server session
       
   235                 return Get(self.eid)
       
   236         self._cw_set_defaults()
       
   237         values = self._to_gae_dict(convert=False)
       
   238         parent = key_name = _app = None
       
   239         if self._gaeinitargs is not None:
       
   240             args, kwargs = self._gaeinitargs
       
   241             args = list(args)
       
   242             if args:
       
   243                 parent = args.pop(0)
       
   244             if args:
       
   245                 key_name = args.pop(0)
       
   246             if args:
       
   247                 _app = args.pop(0)
       
   248             assert not args
       
   249             if 'parent' in kwargs:
       
   250                 assert parent is None
       
   251                 parent = kwargs.pop('parent')
       
   252             if 'key_name' in kwargs:
       
   253                 assert key_name is None
       
   254                 key_name = kwargs.pop('key_name')
       
   255             if '_app' in kwargs:
       
   256                 assert _app is None
       
   257                 _app = kwargs.pop('_app')
       
   258 
       
   259             for key, value in kwargs.iteritems():
       
   260                 if key in self._attributes:
       
   261                     values['s_'+key] = value
       
   262         else:
       
   263             kwargs = None
       
   264         if key_name is None:
       
   265             key_name = self.db_key_name()
       
   266             if key_name is not None:
       
   267                 key_name = 'key_' + key_name
       
   268         for key, value in values.iteritems():
       
   269             if value is None:
       
   270                 continue
       
   271             values[key] = self._cubicweb_to_datastore(key, value)
       
   272         entity = Entity(self.id, parent, _app, key_name)
       
   273         entity.update(values)
       
   274         init_relations(entity, self.e_schema)
       
   275         return entity
       
   276 
       
   277     def db_key_name(self):
       
   278         """override this method to control datastore key name that should be
       
   279         used at entity creation.
       
   280 
       
   281         Note that if this function return something else than None, the returned
       
   282         value will be prefixed by 'key_' to build the actual key name.
       
   283         """
       
   284         return None
       
   285 
       
   286     def metainformation(self):
       
   287         return {'type': self.id, 'source': {'uri': 'system'}, 'extid': None}
       
   288 
       
   289     def view(self, vid, __registry='views', **kwargs):
       
   290         """shortcut to apply a view on this entity"""
       
   291         return self.vreg[__registry].render(vid, self.req, rset=self.rset,
       
   292                                            row=self.row, col=self.col, **kwargs)
       
   293 
       
   294     @classmethod
       
   295     def _rest_attr_info(cls):
       
   296         mainattr, needcheck = super(Model, cls)._rest_attr_info()
       
   297         if needcheck:
       
   298             return 'eid', False
       
   299         return mainattr, needcheck
       
   300 
       
   301     def get_value(self, name):
       
   302         try:
       
   303             value = self[name]
       
   304         except KeyError:
       
   305             if not self.has_eid():
       
   306                 return None
       
   307             value = self._dbmodel.get('s_'+name)
       
   308             if value is not None:
       
   309                 if isinstance(value, Text):
       
   310                     value = unicode(value)
       
   311                 elif isinstance(value, Blob):
       
   312                     value = Binary(str(value))
       
   313             self[name] = value
       
   314         return value
       
   315 
       
   316     def has_eid(self):
       
   317         if self.eid is None:
       
   318             return False
       
   319         try:
       
   320             Key(self.eid)
       
   321             return True
       
   322         except BadKeyError:
       
   323             return False
       
   324 
       
   325     def complete(self, skip_bytes=True):
       
   326         pass
       
   327 
       
   328     def unrelated(self, rtype, targettype, role='subject', limit=None,
       
   329                   ordermethod=None):
       
   330         # XXX dumb implementation
       
   331         if limit is not None:
       
   332             objs = Query(str(targettype)).Get(limit)
       
   333         else:
       
   334             objs = Query(str(targettype)).Run()
       
   335         return rset_from_objs(self.req, objs, ('eid',),
       
   336                               'Any X WHERE X is %s' % targettype)
       
   337 
       
   338     def key(self):
       
   339         return Key(self.eid)
       
   340 
       
   341     def put(self, req=None):
       
   342         if req is not None and self.req is None:
       
   343             self.req = req
       
   344         dbmodel = self.to_gae_model()
       
   345         key = Put(dbmodel)
       
   346         self.eid = str(key)
       
   347         if self.req is not None and self.rset is None:
       
   348             self.rset = rset_from_objs(self.req, dbmodel, ('eid',),
       
   349                                        'Any X WHERE X eid %(x)s', {'x': self.eid})
       
   350             self.row = self.col = 0
       
   351         return dbmodel
       
   352 
       
   353     @needrequest
       
   354     def get(cls, req, keys):
       
   355         # if check if this is a dict.key call
       
   356         if isinstance(cls, Model) and keys in cls._attributes:
       
   357             return super(Model, cls).get(keys)
       
   358         rset = rset_from_objs(req, Get(keys), ('eid',),
       
   359                               'Any X WHERE X eid IN %(x)s', {'x': keys})
       
   360         return list(rset.entities())
       
   361 
       
   362     @needrequest
       
   363     def get_by_id(cls, req, ids, parent=None):
       
   364         if isinstance(parent, Model):
       
   365             parent = parent.key()
       
   366         ids, multiple = NormalizeAndTypeCheck(ids, (int, long))
       
   367         keys = [Key.from_path(cls.kind(), id, parent=parent)
       
   368                 for id in ids]
       
   369         rset = rset_from_objs(req, Get(keys))
       
   370         return list(rset.entities())
       
   371 
       
   372     @classmethod
       
   373     def get_by_key_name(cls, req, key_names, parent=None):
       
   374         if isinstance(parent, Model):
       
   375             parent = parent.key()
       
   376         key_names, multiple = NormalizeAndTypeCheck(key_names, basestring)
       
   377         keys = [Key.from_path(cls.kind(), name, parent=parent)
       
   378                 for name in key_names]
       
   379         rset = rset_from_objs(req, Get(keys))
       
   380         return list(rset.entities())
       
   381 
       
   382     @classmethod
       
   383     def get_or_insert(cls, req, key_name, **kwds):
       
   384         def txn():
       
   385             entity = cls.get_by_key_name(key_name, parent=kwds.get('parent'))
       
   386             if entity is None:
       
   387                 entity = cls(key_name=key_name, **kwds)
       
   388                 entity.put()
       
   389             return entity
       
   390         return RunInTransaction(txn)
       
   391 
       
   392     @classmethod
       
   393     def all(cls, req):
       
   394         rset = rset_from_objs(req, Query(cls.id).Run())
       
   395         return list(rset.entities())
       
   396 
       
   397     @classmethod
       
   398     def gql(cls, req, query_string, *args, **kwds):
       
   399         raise NotImplementedError('use rql')
       
   400 
       
   401     @classmethod
       
   402     def kind(cls):
       
   403         return cls.id
       
   404 
       
   405     @classmethod
       
   406     def properties(cls):
       
   407         raise NotImplementedError('use eschema')
       
   408 
       
   409     def dynamic_properties(self):
       
   410         raise NotImplementedError('use eschema')
       
   411 
       
   412     def cw_is_saved(self):
       
   413         return self.has_eid()
       
   414 
       
   415     def parent(self):
       
   416         parent = self._dbmodel.parent()
       
   417         if not parent is None:
       
   418             rset = rset_from_objs(self.req, (parent,), ('eid',),
       
   419                                   'Any X WHERE X eid %(x)s', {'x': parent.key()})
       
   420             parent = rset.get_entity(0, 0)
       
   421         return parent
       
   422 
       
   423     def parent_key(self):
       
   424         return self.parent().key()
       
   425 
       
   426     def to_xml(self):
       
   427         return self._dbmodel.ToXml()
       
   428 
       
   429 # hijack AnyEntity class
       
   430 entities.AnyEntity = Model
       
   431 
       
   432 BooleanProperty = db.BooleanProperty
       
   433 URLProperty = db.URLProperty
       
   434 DateProperty = db.DateProperty
       
   435 DateTimeProperty = db.DateTimeProperty
       
   436 TimeProperty = db.TimeProperty
       
   437 StringProperty = db.StringProperty
       
   438 TextProperty = db.TextProperty
       
   439 BlobProperty = db.BlobProperty
       
   440 IntegerProperty = db.IntegerProperty
       
   441 FloatProperty = db.FloatProperty
       
   442 ListProperty = db.ListProperty
       
   443 SelfReferenceProperty = db.SelfReferenceProperty
       
   444 UserProperty = db.UserProperty
       
   445 
       
   446 
       
   447 class ReferencePropertyStub(object):
       
   448     def __init__(self, cls, args, kwargs):
       
   449         self.cls = cls
       
   450         self.args = args
       
   451         self.kwargs = kwargs
       
   452         self.required = False
       
   453         self.__dict__.update(kwargs)
       
   454         self.creation_counter = db.Property.creation_counter
       
   455         db.Property.creation_counter += 1
       
   456 
       
   457     @property
       
   458     def data_type(self):
       
   459         class FakeDataType(object):
       
   460             @staticmethod
       
   461             def kind():
       
   462                 return self.cls.__name__
       
   463         return FakeDataType
       
   464 
       
   465 def ReferenceProperty(cls, *args, **kwargs):
       
   466     if issubclass(cls, db.Model):
       
   467         cls = db.class_for_kind(cls.__name__)
       
   468         return db.ReferenceProperty(cls, *args, **kwargs)
       
   469     return ReferencePropertyStub(cls, args, kwargs)