# -*- 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.
#-----------------------------------------------------------------------------
"""
... describe me please I feel alone...
CVS information
$Id: Attribute.py 968 2006-02-25 15:46:25Z sbigaret $
"""
__version__='$Revision: 968 $'[11:-2]
# Modeling
from Modeling.utils import isaValidName,toBoolean
from Modeling.XMLutils import *
from Modeling.Model import ModelError
from Modeling.KeyValueCoding import KeyValueCoding
from Modeling import Validation
from Modeling.utils import capitalizeFirstLetter
# Zope 2.4
try:
from Products.PluginIndexes.TextIndex.TextIndex import TextIndex
from Products.PluginIndexes.FieldIndex.FieldIndex import FieldIndex
from Products.PluginIndexes.KeywordIndex.KeywordIndex import KeywordIndex
except:
class Index:
def __init__(self, meta_type):
self.meta_type=meta_type
TextIndex=Index('TextIndex')
FieldIndex=Index('FieldIndex')
KeywordIndex=Index('KeywordIndex')
# python
import types, string, sys, time
from cgi import escape
from mx.DateTime import DateTime,DateFrom,Time
# Module constants
# useless, should be dropped
NOT_INDEXED = 'Not indexed'
TEXT_INDEX = TextIndex.meta_type
FIELD_INDEX = FieldIndex.meta_type
KEYWORD_INDEX = KeywordIndex.meta_type
SUPPORTED_INDEX_TYPE_NAMES= (NOT_INDEXED,
TEXT_INDEX,
FIELD_INDEX,
KEYWORD_INDEX)
SUPPORTED_INDEX_TYPES = (None,
TextIndex,
FieldIndex,
KeywordIndex)
def date_types():
"Returns the set of valid date types"
avail_types=[ type(DateTime(0)), type(Time(0)) ]
try:
import DCOracle2
avail_types.append(type(DCOracle2.Date(0,0,0)))
except ImportError:
pass
try:
from time import localtime
from datetime import date,datetime
avail_types.append( type(datetime(*localtime(0)[:3])) )
avail_types.append( type(date(*localtime(0)[:3])) )
except ImportError:
pass
return avail_types
def availableTypes():
#return ('string'(py2.1)/'str'(py2.2), 'int', 'float', 'tuple')
avail_types=[ types.StringType,
types.IntType,
types.FloatType,
#types.TupleType,
]
avail_types.extend(date_types())
return tuple([t.__name__ for t in avail_types])
from Modeling.utils import base_persistent_object
class Attribute(base_persistent_object, XMLCapability, KeyValueCoding):
"Describes an attribute"
# + public/private _TBD
# _TBD check: defaultValue of correct type
#
_isClassProperty=1
_definition=''
_isFlattened=0
_isDerived=0
_precision=0
_scale=0
_externalType=''
_columnName=''
_width=0
_comment=''
def __init__(self, aName='', anEntity=None):
"Initializes an attribute. A name **must** be provided."
if (not aName):# or (not anEntity):
raise ModelError, "Attribute: A name should be provided at creation time"
assert type(aName)==types.StringType, "parameter name should be a string (type: <pre>'%s'</pre>, value: <pre>'%s'</pre>)" % (escape(str(type(aName)),1), escape(str(aName),1))
#assert type(anEntity)==types.InstanceType and anEntity.__class__ is Entity
if not isaValidName(aName):
raise ModelError, "The name '%s' is not a valid name"
self._allowsNone=1
self._columnName=''
self._comment=''
self._defaultValue=None
self._definition=''
self._displayLabel=''
self._entity=None
self._externalType=''
self._name=aName
self._isClassProperty=1
self._isDerived=0
self._isFlattened=0
self._precision=0
self._scale=0
self._type = types.StringType.__name__
self._width=0
if anEntity:
anEntity.addAttribute(self)
def allowsNone(self):
"""
Indicates whether the attribute can be empty (None) or not
(see isRequired inverse method)
"""
return self._allowsNone
allowsNull=allowsNone
def availableTypes(self):
"Returns all available types"
return availableTypes()
def availableIndexTypeNames(self):
"Returns all available type names, as a tuple"
return SUPPORTED_INDEX_TYPE_NAMES
def clone(self, name=None):
"""
Returns a copy of the current Attribute
Parameter:
name -- optional. If provided the clone gets that name, otherwise
defaults to 'self.name()'.
See also: Entity.clone()
"""
clone=Attribute(name or self.name())
# the mechanism used here relies on functionalities decl. in XMLCapability
for prop in self.fullPropertySet().keys():
clone.xmlSetAttribute(prop)(self.xmlGetAttribute(prop)())
return clone
def columnName(self):
"..."
return self._columnName
def comment(self):
"Returns the comment field"
return self._comment
def defaultValue(self):
"Returns the default value associated to this attribute"
return self._defaultValue
def defaultValueAsPythonStatement(self):
"""
Returns the default value in a string that can be used in python code,
e.g. in assignments
This is mainly for use in the generation of templates of code
"""
if self.defaultValue()==None:
return 'None'
my_type=self.type()
if my_type in ('string', 'str'):
#if self.defaultValue() is None: return 'None'
import re
return "'%s'"%re.sub("'", "\\'", self.defaultValue())
if my_type=='int': return str(self.defaultValue())
if my_type=='float': return str(self.defaultValue())
#if my_type=='tuple': return str(self.defaultValue())
if my_type=='float': return str(self.defaultValue())
if my_type=='DateTime': return "DateTimeFrom('%s')"%str(self.defaultValue())
return None
def definition(self):
"Returns the definition for a flattened or derived attribute"
return self._definition
def displayLabel(self):
"Returns the label associated to the current attribute"
return self._displayLabel
def entity(self):
"Returns the associated 'Entity'"
return self._entity
parent=entity
def externalType(self):
"""
"""
return self._externalType
def finalAttribute(self):
"""
Returns the final attribute pointed by a flattened attribute.
Returns None if none can be found, or if the attribute is not flattened
See also: isFlattened(), definition(), relationshipPath(),
relationshipPathObjects()
"""
if not self.isFlattened(): return None
attr=None
path=string.split(self._definition, '.')
try:
finalRel=self.relationshipPathObjects()[-1]
attr=finalRel.destinationEntity().attributeNamed(path[-1])
except:
pass
return attr
def isClassProperty(self):
"Indicates whether the attribute belongs to the class properties/fields"
return self._isClassProperty
def isDerived(self):
"""
Tells whether the attribute is derived. Note that a flattened attribute
is also considered derived
"""
return (self._isDerived or self._isFlattened)
def isFlattened(self):
"Indicates whether the attribute is flattened"
return self._isFlattened
def isNotClassProperty(self):
"negation of isClassProperty"
return not self.isClassProperty()
#def isPrivate(self):
# "Indicates whether the attribute is private"
# return not self.isPublic()
#
#def isPublic(self):
# "Indicates whether the attribute is public"
# return self._isPublic
def name(self):
"Returns the attribute's name "
return self._name
def isRequired(self):
"Returns whether the attribute is required (see also: allowsNone)"
return not self._allowsNone
def precision(self):
"""
"""
return self._precision
def readFormat(self):
"""
Simply returns the attribute's columnName()
"""
return self.columnName()
def relationshipPath(self):
"""
Returns the relationshipPath (string) for a flattened attribute, or
None if the attribute is not flattened
The relationshipPath consists of the definition() from which the last
part (which designates the finalAttribute()) is stripped off.
For example, the relationshipPath for an attribute whose definition is
'relationship1.relationship2.attribute' is 'relationship1.relationship2'.
See also: isFlattened(), definition(), finalAttribute(),
relationshipPathObjects()
"""
if not self.isFlattened(): return None
return string.join(string.split(self.definition(), '.')[:-1], '.')
def relationshipPathObjects(self):
"""
Returns the list ('tuple')of Relationship objects that should be traversed
to obtain the finalAttribute(), or None if the attribute is not flattened
See also: isFlattened(), definition(), finalAttribute(),
relationshipPath()
"""
if not self.isFlattened(): return None
path=string.split(self._definition, '.')
rels=[]
currentEntity=self.entity()
for relName in path[:-1]:
rel=currentEntity.relationshipNamed(relName)
rels.append(rel)
currentEntity=rel.destinationEntity()
return tuple(rels)
def scale(self):
"""
"""
return self._scale
def setAllowsNone(self, allowsNone):
"""
Tells the attribute whether it may have 'None' value or not
(see inverse method setIsRequired)
"""
self._allowsNone=toBoolean(allowsNone)
def setColumnName(self, columnName):
"..."
self._columnName=columnName
def setComment(self, aComment):
"Sets the comment field"
self._comment=aComment
def convertStringToAttributeType(self, aValue):
"""
Internally used to convert a string value (taken from a GUI, for example)
to the correct type.
See: setDefaultValue()
"""
# NB: types.StringType.__name__: 'string' in py2.1, 'str' in py2.2
if aValue=='None':
if self.type() in availableTypes():
return None
else:
raise ModelError, \
"Invalid 'None' default value for attribute '%s' (type: %s)"%(self._name, self.type())
if not aValue:
if self.type() in ('string', 'str'): return ""
if self.type()=='int': return 0
if self.type()=='float': return 0.0
#if self.type()=='tuple': return ()
if self.type()=='DateTime': return DateFrom(time.time())
try:
if self.type() in ('string', 'str'): return str(aValue)
if self.type()=='int': return int(aValue)
if self.type()=='float': return float(aValue)
#if self.type()=='tuple':
# _tmp=eval(aValue)
# if type(_tmp)!=types.TupleType: raise ValueError
# return _tmp
if self.type()=='DateTime':
if type(aValue) not in date_types():
# Some python db-adapters, such as MySQLdb, already return the
# appropriate type
return DateFrom(aValue)
return aValue
except (ValueError, TypeError):
raise ModelError, \
"Invalid value ('%s') for attribute '%s' type '%s' "%(aValue, self._name,self.type())
def setScale(self, scale):
"""
"""
self._scale=scale
def setDefaultValue(self, aValue):
"Sets the default value associated to this attribute"
self._defaultValue=self.convertStringToAttributeType(aValue)
def setDefinition(self, definition):
"Sets the definition for a flattened or derived attribute"
self._definition=definition
def setDisplayLabel(self, aName):
"Sets the label associated to the attribute"
self._displayLabel = aName
def _setEntity(self, anEntity):
"Binds the attribute with the supplied entity"
self._entity=anEntity
def setExternalType(self, externalType):
"""
"""
self._externalType=externalType
def setIsClassProperty(self, aBool):
"Tells the receiver whether it belongs to the class properties/fields"
self._isClassProperty = toBoolean(aBool)
def setIsFlattened(self, aBool):
"""
Tells the attribute that it is flattened
See also: setDefinition()
"""
self._isFlattened = toBoolean(aBool)
def setPrecision(self, precision):
"""
"""
self._precision=int(precision)
def setWidth(self, width):
"Sets the maximum size for that attribute"
self._width=int(width)
#def setIsPrivate(self, aBool):
# "Tells the attribute whether it is private"
# self._isPublic = not toBoolean(aBool)
#
#def setIsPublic(self, aBool):
# "Tells the attribute whether it is public"
# self._isPublic = toBoolean(aBool)
def setIsRequired(self, isRequired):
"Tells the attribute whether is required or not (inverse of setAllowNone)"
self._allowsNone=not toBoolean(isRequired)
def setName(self, aName):
"Sets the attribute's name. Raises if invalid"
if not isaValidName(aName):
raise ModelError, "Supplied name incorrect (%s)" % aName
oldName=self._name
self._name = aName
if self._entity:
self._entity.propertyNameDidChange(oldName)
def setType(self, aType, check=1):
"""
Sets the type associated to the attribute.
aType object should be an available type's name
(see Modeling.Attribute.availableTypes() for a complete list
Specify 'check=0' if you do not want the 'defaultValue' to be
verified against the new type.
"""
if sys.version_info >= (2, 2): # python v2.2 and higher
# backward compatibility, for models generated with python<=2.1
if aType=='string':
aType='str'
if aType in availableTypes():
self._type = aType
else:
raise ModelError, "Invalid parameter aType ('%s')" % str(aType)
if check: self.setDefaultValue(str(self._defaultValue))
def type(self):
"""
Returns the type associated to the attribute.
"""
return self._type
def validateValue(self, value, object=None):
"""
Checks whether the supplied object is valid, i.e. the following tests are
made:
1. value cannot be *void* ('not value' equals to 1) if attribute is
required
2. value has the same type than the attribute's one
3. if self.type() is 'string' and self.width() is set and non-zero, the
length of 'value' should not exceeded self.width()
4. (Not implemented yet) if attribute is constrained, the value is part
of the constrained set (repeat: not implemented yet)
Parameter 'value' is required, and 'object' is optional --it is
only used to make the error message clearer in case of failure.
Silently returns if it succeeds.
In case of failure, it raises 'Modeling.Validation.ValidationException' ;
in this case, exception's *value* ('sys.exc_info[1]') consists in a set
of lines, describing line per line the different tests which failed.
"""
#if object is None: # or object is None:
# raise ValueError, "Parameter 'object' is required"
_error=Validation.ValidationException()
# isRequired?
if value is None and self.isRequired():
_error.addErrorForKey(Validation.REQUIRED, self.name())
# correct types?
if value is not None:
# int and long int are equivalent
if type(value)==types.LongType:
if self.type()!=types.IntType.__name__:
_error.addErrorForKey(Validation.TYPE_MISMATCH, self.name())
elif type(value) in date_types():
if self.type() not in [t.__name__ for t in date_types()]:
_error.addErrorForKey(Validation.TYPE_MISMATCH, self.name())
elif type(value).__name__!=self.type():
_error.addErrorForKey(Validation.TYPE_MISMATCH, self.name())
# String should not exceed width
if type(value)==types.StringType and self.type()=='string' and \
self.width():
if len(value)>self.width():
_error.addErrorForKey(Validation.TYPE_MISMATCH, self.name())
#print 'len(value): %s != self.width(): %s'%(len(value),self.width())
_error.finalize()
return
def width(self):
"""
Returns the maximum width for this attribute. Should be '0' (zero) for
date and numbers
"""
return self._width
def __str__(self):
"""
Returns a printable representation of the attribute ; the format is
<entityName>.<attributeName>.
If self.entity() is not already set, then it returns None.
Note: this string uniquely identificates an attribute in a given ModelSet
or Model since entities'names share the same namespace
"""
_entity=self.entity()
if _entity:
return _entity.name()+'.'+self.name()
else:
return ""
## XML
def initWithXMLDOMNode(self, aNode, encoding='iso-8859-1'):
"""
Initializes a model with the supplied xml.dom.node.
"""
k_v=aNode.attributes.items()
# Now we must make sure that the type is initialized BEFORE the default
# value is set --> we simply make sure that this will be the first one
# to be initialized
try:
t=[a for a in k_v if a[0]=='type'][0] #IndexError
k_v.remove(t)
k_v=[t]+k_v
except IndexError: pass
for attributeName, value in k_v:
# Iterate on attributes declared in node
attrType=self.xmlAttributeType(attributeName)
set=self.xmlSetAttribute(attributeName)
if attrType=='string': value=unicodeToStr(value, encoding)
elif attrType=='number': value=int(value)
elif attrType=='bool': value=int(value)
set(value)
def getXMLDOM(self, doc=None, parentNode=None, encoding='iso-8859-1'):
"""
Returns the (DOM) DocumentObject for the receiver.
Parameters 'doc' and 'parentDoc' should be both omitted or supplied.
If they are omitted, a new DocumentObject is created.
If they are supplied, elements are added to the parentNode.
Returns: the (possibly new) DocumentObject.
"""
if (doc is None) ^ (parentNode is None):
raise ValueError, "Parameters 'doc' and 'parentNode' should be together supplied or omitted"
if doc is None:
doc=createDOMDocumentObject('attribute')
parentNode=doc.documentElement
node=parentNode
else:
node=doc.createElement('attribute')
parentNode.appendChild(node)
exportAttrDict=self.xmlAttributesDict(select=1)
for attr in exportAttrDict.keys():
attrType=self.xmlAttributeType(attr)
value=self.xmlGetAttribute(attr)()
if attrType=='bool': value=int(value)
value=strToUnicode(str(value), encoding)
node.setAttribute(attr, value)
return doc
def xmlAttributesDict(self, select=0):
"-"
if not select:
return self.fullPropertySet()
else:
if not self.isDerived():
return self.nonDerivedPropertySet()
elif self._isFlattened:
return self.flattenedPropertySet()
raise NotImplementedError, 'All cases are not properly covered!'
##
# PropertySets
##
def fullPropertySet(self):
setType=self.setType
return {
'name' : ('string',
lambda self=None,p=None: None,
self.name),
'columnName' : ('string',
self.setColumnName,
self.columnName),
'comment' : ( 'string',
self.setComment,
self.comment),
'displayLabel' : ('string',
self.setDisplayLabel,
self.displayLabel),
'externalType' : ('string',
self.setExternalType,
self.externalType),
'isClassProperty' : ('bool',
self.setIsClassProperty,
self.isClassProperty),
'isRequired' : ('bool',
self.setIsRequired,
self.isRequired),
# defaultValue must be loaded AFTER type is set, or we will get the
# wrong value
'defaultValue' : ('string',
self.setDefaultValue,
self.defaultValue),
'definition' : ('string',
self.setDefinition,
self.definition),
'precision' : ('string',
self.setPrecision,
self.precision),
'scale' : ('string',
self.setScale,
self.scale),
'type' : ('string',
lambda aType, setType=setType:\
setType(aType=aType,
check=0),
self.type),
'isFlattened' : ('bool',
self.setIsFlattened,
self.isFlattened),
'width' : ('string',
self.setWidth,
self.width),
}
def nonDerivedPropertySet(self):
# in sense of isDerived(), NOT of self._isDerived
subList=('name',
'columnName', 'precision', 'scale', 'width', 'externalType', #DB
'comment', 'displayLabel', 'isClassProperty', 'isRequired',
'defaultValue', 'type', )
dict=self.fullPropertySet()
deleteKeys=filter(lambda k, list=subList: k not in list, dict.keys())
for key in deleteKeys: del dict[key]
return dict
def flattenedPropertySet(self):
subList=('name', 'displayLabel', 'isClassProperty', 'definition',
'isFlattened', 'comment')
dict=self.fullPropertySet()
deleteKeys=filter(lambda k, list=subList: k not in list, dict.keys())
for key in deleteKeys: del dict[key]
return dict
##
##
def validateAttribute(self):
"""Validates the attributes against general model consistency:
- attribute cannot be both public and catalogable
- defaultValue should have the correct type
- no columnName() for flattened attributes
- etc. TBD
"""
raise NotImplementedError
##
## KeyValueCoding error handling
##
def handleAssignementForUnboundKey(self, value, key):
if key=='doc': self.setComment(value)
else:
raise AttributeError, key
handleTakeStoredValueForUnboundKey=handleAssignementForUnboundKey
|