# -*- coding: iso-8859-1 -*-
#-----------------------------------------------------------------------------
# Modeling Framework: an Object-Relational Bridge for python
#
# Copyright (c) 2001-2004 Sbastien Bigaret <sbigaret@users.sourceforge.net>
# All rights reserved.
#
# This file is part of the Modeling Framework.
#
# This code is distributed under a "3-clause BSD"-style license;
# see the LICENSE file for details.
#-----------------------------------------------------------------------------
"""
CustomObject
CVS information
$Id: CustomObject.py 932 2004-07-20 06:21:57Z sbigaret $
"""
__version__='$Revision: 932 $'[11:-2]
# framework
import ClassDescription
from DatabaseObject import DatabaseObject
from KeyValueCoding import KeyValueCoding
import Validation
from RelationshipManipulation import RelationshipManipulation
from FaultHandler import FaultHandler,AccessArrayFaultHandler
import ObserverCenter
from EditingContext import ObjectNotRegisteredError
from utils import capitalizeFirstLetter
# interfaces
from interfaces.DatabaseObject import DatabaseObjectInterface
class Snapshot_ToManyFault:
"""
Snapshot_ToManyFault is used in CustomObject.snapshot(), when returning
the value for a to-many relationship which is still a fault.
It should not be mistaken for FaultHandler.AccessArrayFaultHandler, which
holds the real to-many fault. An instance of this class is just a mean for
CustomObject.snapshot() to tell that it found a fault. If you need the real
to-many fault, use getToManyFault().
See also: CustomObject.snapshot() for additional details.
"""
def __init__(self, sourceGlobalID, key):
"""Initializer
Parameter:
sourceGlobalID -- a non temporary GlobalID (this is a non sense to have
a to many fault for an object that has just been inserted
key -- the corresponding to-many relationship's name
Raises ValueError is sourceGlobalID.isTemporary() is true.
"""
if sourceGlobalID.isTemporary():
raise ValueError, 'sourceGlobalID cannot be a temporary global id'
self.sourceGlobalID=sourceGlobalID
self.key=key
def getToManyFault(self, ec):
"""
Returns the real to-many fault that this object represents.
Parameter:
ec -- an EditingContext
"""
return ec.arrayFaultWithSourceGlobalID(self.sourceGlobalID, self.key,
ec)
from Modeling.utils import base_object
class CustomObject(base_object, RelationshipManipulation, DatabaseObject):
"""
Validation
See Validation for details
"""
__implements__ = (DatabaseObjectInterface,)+DatabaseObject.__implements__
_globalID=None
__editingContext=lambda self: None
# The following is needed: when a fault is triggered and belongs to an
# entity participating to an inheritance hierarchy, the possibly already
# cached classDescription should be reset.
# (see also: DatabaseChannel.fetchObject)
_v_classDescription=None
def awakeFromFetch(self, anEditingContext):
"""
Automatically called by the framework when an object is initialized from
its database row.
Implementation simply sends 'awakeObjectFromFetch()' to the object's
class description.
Subclasses can override this method ; if they do, they must make sure that
this method is called before making their own initialization.
See also: ClassDescription.awakeObjectFromFetch(), awakeFromInsertion()
"""
self.classDescription().awakeObjectFromFetch(self, anEditingContext)
def awakeFromInsertion(self, anEditingContext):
"""
Automatically called by the framework when an object is inserted
in an EditingContext.
Implementation simply sends 'awakeObjectFromInsertion()' to the object's
class description.
Subclasses can override this method ; if they do, they must make sure that
this method is called before making their own initialization.
See also: ClassDescription.awakeObjectFromInsertion(), awakeFromFetch()
"""
self.classDescription().awakeObjectFromInsertion(self, anEditingContext)
def allPropertyKeys(self):
"""
Returns the whole set of properties, made of attributesKeys(),
toManyRelationshipKeys() and toOneRelationshipKeys().
"""
return self.attributesKeys()+\
self.toManyRelationshipKeys()+self.toOneRelationshipKeys()
def attributesKeys(self):
"""
Forwards the message to the object's classDescription() and returns the
result.
In most cases, if not all, the underlying classDescription() is an
EntityClassDescription instance, which only returns attributes that are
marked as 'class properties' in the model.
See also: toOneRelationshipKeys, toManyRelationshipKeys
"""
return self.classDescription().attributesKeys()
def classDescription(self):
"""
Returns the object's corresponding ClassDescription ; in most cases
this is an EntityClassDescription instance.
"""
cd=getattr(self, '_v_classDescription', None)
if not cd:
cd=ClassDescription.classDescriptionForName(self.entityName())
self._v_classDescription=cd
return cd
def classDescriptionForDestinationKey(self, aKey):
"""
Returns the ClassDescription used for objects that can be referenced
by the relationship 'aKey'.
CustomObject's implementation simply forwards the message to the
underlying classDescription() and returns the result.
"""
return self.classDescription().classDescriptionForDestinationKey(aKey)
#def clearProperties(self):
# DatabaseObject.clearProperties.im_func(self)
def deleteRuleForRelationshipKey(self, aKey):
"""
CustomObject's implementation simply forwards the message to the
underlying classDescription() and returns the result.
"""
return self.classDescription().deleteRuleForRelationshipKey(aKey)
def entityName(self):
raise 'MethodShouldBeOverridden', 'Subclass should override this method'
def editingContext(self):
"""
Returns the EditingContext in which the object was registered, or 'None'
"""
return self.__editingContext()
def globalID(self):
"""Returns the object's globalID, or None if the object is not registered
within an EditingContext"""
ec=self.editingContext()
return ec and ec.globalIDForObject(self) or None
def inverseForRelationshipKey(self, aKey):
"""
CustomObject's implementation simply forwards the message to the
underlying classDescription() and returns the result.
Sometimes the framework is not able to calculate the correct relationship.
For example, it might be that a relationship and its inverse are not based
on inverse joins, such as in a one-to-one association, say between a
Writer and its PostalAddress, where both relationships store the primary
key of the destination entity in a foreign key of their own (I do not want
to argue here about such a design, this can happen if you have to work on
an existing database). In such a case, you will need to override this
method so that the framework can find the inverse relationship. This could
be done that way in class Writer::
def inverseForRelationshipKey(self, aKey):
\"\"\"
Overrides CustomObject.inverseForRelationshipKey() so that
'toPostalAddress' relationship uses the inverse relationship
'PostalAddress.toAuthor'
\"\"\"
if aKey=='toPostalAddress': return 'toAuthor'
else: # call super
return CustomObject.inverseForRelationshipKey(self, aKey)
Of course, the same needs to be done in class PostalAddress.
"""
return self.classDescription().inverseForRelationshipKey(aKey)
def isToManyKey(self, aKey):
return aKey in self.toManyRelationshipKeys()
def propagateDeleteWithEditingContext(self, editingContext):
"""
Handles the deletion of the current object according to the delete
rules which apply to its relationships, as defined in the model.
CustomObject's implementation simply forwards the message to the
underlying classDescription() [using propagateDeleteForObject] and returns
the result.
Subclasses overriding this method should call this (superclass)
method.
See also: ClassDescription.propagateDeleteForObject()
Relationship.deleteRule()
"""
self.classDescription().propagateDeleteForObject(self, editingContext)
def propagateInsertionWithEditingContext(self, editingContext):
"""
Called by an EditingContext when a object is marked as inserted or
updated.
CustomObject's implementation simply forwards the method to the underlying
classDescription() (sending it a propagateInsertionForObject message).
Subclasses overriding this method should call this (superclass)
method.
See also: ClassDescription.propagateInsertionForObject()
"""
self.classDescription().propagateInsertionForObject(self, editingContext)
def snapshot(self):
"""
Returns the snapshot for the current object, i.e. a dictionary whose keys
are allPropertyKeys(), and whose values are :
- for attribute, the corresponding value,
- for toOne relationships, the GlobalID of the related object (or None).
If you want to get the real object corresponding to that global id,
simply call EditingContext.faultForGlobalID() on
self.editingContext().
- For toMany relationships, the returned value depends on whether the
set of related objects is still a fault or not:
- if it is still a to-many fault, you'll get an instance of
Snapshot_ToManyFault. To get the real to-many fault (instance of
FaultHandler.AccessArrayFaultHandler), simply call
getToManyFault() on this instance,
- otherwise, you get the list of the GlobalIDs of the related
objects
Why are to-many faults handled this way? We do not want snapshot() to
trigger any round-trip to the database. One could say, okay, but then
you could return the to-many fault as well. True, but this won't be
consistent with the other values returned by snapshot: values for
to-one relationship are globalIDs with which you must explicitely call
ec.faultForGlobalID() to get the real object. Same for to-many faults:
you also need to explicitely call Snapshot_ToManyFault's
getToManyFault() to get the whole (faulted) array. Last, this also
prevents a to-many fault to be cleared by mistake (because simply
looking at one of the fault properties, such as itys length, triggers
a round-trip to the database).
Raises ObjectNotRegisteredError if the object itself is not registered in
an EditingContext, or if any of the objects it is in relation w/ is not
known to the object's editingContext().
"""
ec=self.editingContext()
if ec is None:
raise ObjectNotRegisteredError, 'Unable to compute snapshot: no editingContext()'
res={}
toOneKeys=self.toOneRelationshipKeys()
toManyKeys=self.toManyRelationshipKeys()
relKeys=toOneKeys+toManyKeys
for key in self.allPropertyKeys():
value=self.storedValueForKey(key)
if key in toOneKeys:
if value is None:
res[key]=None
elif isinstance(value, FaultHandler):
res[key]=value.globalID()
else:
res[key]=ec.globalIDForObject(value)
if value and res[key] is None:
raise ObjectNotRegisteredError,\
"Object %s at %s references object %s at %s for key '%s' but "\
"the latter is not registered within the EditingContext %s "\
"at %s"%\
(str(self), hex(id(self)),str(value), hex(id(value)), key,
ec, hex(id(ec)))
elif key in toManyKeys:
if not isinstance(value, FaultHandler):
res[key]=map(lambda o, ec=ec: ec.globalIDForObject(o), value)
if None in res[key]:
raise ObjectNotRegisteredError,\
"Object %s at %s references a list of object (%s) for "\
"key '%s' but the latter holds at least one object that is "\
"not registered within the EditingContext %s at %s"%\
(str(self), hex(id(self)),str(value), key, ec, hex(id(ec)))
else:
res[key]=Snapshot_ToManyFault(self.globalID(), key)
else:
res[key]=value
return res
def snapshot_raw(self):
"""
Returns the raw snapshot for the current object, i.e. a dictionary whose
keys are allAttributesKeys() with their corresponding values.
Special care should be taken when examining the values for primary keys
and foreign keys:
- if your object and all its related objects are already saved in the
database, you'll get the PK's and FKs' values, as expected (a special
case exists where a FK value can get out-of-sync, see below)
- if your object is inserted in an EditingContext but has not been saved
yet, the PK value will be its TemporaryGlobalID, since there is no way
to know the value it will get when it is saved,
- if your object has a to-one relationship to an other object that is
inserted in an EditingContext but not saved yet, the FK's value will
be that related object's TemporaryGlobalID --for the same reason then
above.
- Last, if the object's entity defines a foreign key involved in a
to-many relationship pointing to it but with no inverse (i.e. self's
entity has no to-one relationship using this foreign key), the
returned value will its value stored in the Database's cache managed
by the framework. In other words, this value will be exact after each
saveChanges(), and the first time the object is fetched; afterwards,
if the object on the other side modifies its relation pointing to
'self' (e.g. removes it), the value will be out-of-sync with the
object graph and will remain as-is until the EditingContext saves its
changes again.
Note: this method does not care about PKs or FKs being marked as class
properties. Even when they are class properties, it does NOT look at their
values, instead the returned values are extracted by examining the objects
related to 'self'.
Raises ObjectNotRegisteredError if the object itself is not registered in
an EditingContext, or if any of the objects it is in relation w/ is not
known to the object's editingContext().
"""
ec=self.editingContext()
if ec is None:
raise ObjectNotRegisteredError, 'Unable to compute snapshot_raw: the object is not registered in an EditingContext'
if self.isFault():
self.willRead()
res={}
cd=self.classDescription()
fks=cd.foreignKeys()
pks=cd.primaryKeys()
toOneRels=filter(lambda rel: rel.isToOne(), cd.entity().relationships())
for key in cd.allAttributesKeys():
if key in fks or key in pks: # we take care of this later
continue
try:
res[key]=self.storedValueForKey(key)
except AttributeError:
# This happens when it comes to fill the value for a FK used in a
# relationship pointing to the object, with no to-one inverse
# relationship defined in the object's entity (thus, this fk is not
# in fks=ClassDescription.foreignKeys()
attr=cd.entity().attributeNamed(key)
if attr and not attr.isClassProperty():
if self.globalID().isTemporary():
# cannot compute, insert None
res[key]=None
else:
db=ec.rootObjectStore().objectStoreForObject(self).database()
snap=db.snapshotForGlobalID(self.globalID())
res[key]=snap[key]
# Handle PKs
gid=self.globalID()
if gid.isTemporary():
for pk in pks:
res[pk]=gid
else:
res.update(gid.keyValues())
# Handle FKs
for rel in toOneRels:
key=rel.name()
rel_obj=self.storedValueForKey(key)
if rel_obj is None:
for src_attr in rel.sourceAttributes():
res[src_attr.name()]=None
continue
rel_gid=rel_obj.globalID()
if rel_gid.isTemporary(): # not saved yet: save its TemporaryGID
for src_attr in rel.sourceAttributes():
res[src_attr.name()]=rel_gid
else: # we have a KeyGlobalID, values can be filled in
rel_gid_kv=rel_gid.keyValues()
for src_attr in rel.sourceAttributes():
dst_attr=rel.destinationAttributeForSourceAttribute(src_attr)
res[src_attr.name()]=rel_gid_kv[dst_attr.name()]
return res
def toManyRelationshipKeys(self):
"""
Forwards the message to the object's classDescription() and returns the
result.
In most cases, if not all, the underlying classDescription() is an
EntityClassDescription instance, which only returns toMany relationships
that are marked as 'class properties' in the model.
See also: attributesKeys, toOneRelationshipKeys
"""
return self.classDescription().toManyRelationshipKeys()
def toOneRelationshipKeys(self):
"""
Forwards the message to the object's classDescription() and returns the
result.
In most cases, if not all, the underlying classDescription() is an
EntityClassDescription instance, which only returns toOne relationships
that are marked as 'class properties' in the model.
See also: attributesKeys, toManyRelationshipKeys
"""
return self.classDescription().toOneRelationshipKeys()
def updateFromSnapshot(self, aSnapshotDictionary):
"""
Given a snapshot, update the current object ; note that if the object is
a fault, that fault will be cleared and the object initialized before the
changes are made.
Parameter:
aSnapshotDictionary -- a dictionary, as returned by snapshot()
See also: snapshot
"""
ec=self.editingContext()
if ec is None:
raise ObjectNotRegisteredError, 'Unable to compute snapshot: no editingContext()'
snap=aSnapshotDictionary
self.willRead() # trigger the fault
toOneKeys=self.toOneRelationshipKeys()
toManyKeys=self.toManyRelationshipKeys()
relKeys=toOneKeys+toManyKeys
for key in self.allPropertyKeys():
value=snap[key]
if key in toOneKeys:
if value is not None:
value=ec.faultForGlobalID(value, ec)
elif key in toManyKeys:
if isinstance(value, Snapshot_ToManyFault):
#gID=ec.globalIDForObject(self)
#value=ec.arrayFaultWithSourceGlobalID(gID, key, ec)
value=value.getToManyFault(ec)
else:
try:
value=map(lambda gID, ec=ec: ec.faultForGlobalID(gID, ec), value)
except:
import pdb ; pdb.set_trace()
self.takeStoredValueForKey(value, key)
def willChange(self):
"""
Notifies the observers that the object is about to change.
CustomObject's default implementation calls willRead() on itself before
notifying the ObserverCenter
"""
self.willRead()
ObserverCenter.notifyObserversObjectWillChange(self)
##
## Validation
##
def validateForDelete(self):
"""
Default implementation simply sends validateObjectForDelete() to the
receiver's ClassDescription.
Subclasses overriding this method should call it prior to any other
checks, and exceptions raised here should be aggegated with custom
validation.
See also: EntityClassDescription.validateObjectForDelete()
"""
self.classDescription().validateObjectForDelete(self)
def validateForInsert(self):
"""
Simply calls validateForSave()
"""
self.validateForSave(self)
def validateForUpdate(self):
"""
Simply calls validateForSave()
"""
self.classDescription().validateForSave(self)
def validateForSave(self):
"""
First forwards forwards the message to the object's ClassDescription,
then iterates on class properties so that they are individually
checked --this is done by sending the message 'validateValueForKey()'
to oneself.
Subclasses overriding this method *must* call it prior to any other
checks, and exceptions raised here *must* be aggegated with custom
validation.
See also: allPropertyKeys(), classDescription()
EntityClassDescription.validateObjectForSave(),
EntityClassDescription.classProperties()
"""
self.classDescription().validateObjectForSave(self)
error=Validation.ValidationException()
hasError=0
for key in self.allPropertyKeys():
try:
self.validateValueForKey(self.storedValueForKey(key), key)
except Validation.ValidationException, exc:
error.aggregateException(exc)
hasError=1
if hasError:
error.aggregateError(Validation.OBJECT_WIDE%repr(self),
Validation.OBJECT_WIDE_KEY)
error.finalize()
def validateValueForKey(self, aValue, aKey):
"""
Validate the value hold by 'aKey' attribute/relationship in the object.
Default implementation first forwards the message to the object's
ClassDescription, which checks that the constraints defined in the
underlying model are respected.
Then it searches if the object defines a method named 'validate<AKey>'
(e.g., with 'aKey=lastName', that method's name would be
'validateLastName': you have already noticed that the first letter of the
key is capitalized in the name of the method). If such a method is
actually defined by the object, then it is called. You will typically
define such a method to define your own validation mechanism (per-key
validation).
Subclasses should never override this method. If you want to specify your
own validation mechanism for a given key, say 'name', you should define a
method named 'validateName()' in your class instead, as described above.
See: EntityClassDescription.validateValueForKey()
"""
validationError=Validation.ValidationException()
try:
self.classDescription().validateValueForKey(aValue, aKey)
except Validation.ValidationException, exc:
validationError.aggregateException(exc)
# Custom bizness logic: validate<AttributeName>
validateBizLogic_methodName='validate'+capitalizeFirstLetter(aKey)
validateFunction=None
try:
validateFunction=getattr(self, validateBizLogic_methodName)
except AttributeError:
# Function is not defined, fine
pass
if validateFunction and callable(validateFunction):
try:
validateFunction(aValue)
except Validation.ValidationException, exc:
validationError.aggregateException(exc)
validationError.addErrorForKey(Validation.CUSTOM_KEY_VALIDATION,
aKey)
validationError.finalize()
|