1
0
Fork 0
mirror of https://github.com/yweber/lodel2.git synced 2026-10-15 02:54:02 +02:00

New version of fieldtypes class hierarchy + partial migrationhandler adaptation

This commit is contained in:
Yann 2016-01-13 17:29:08 +01:00
commit bbf33373d0
10 changed files with 240 additions and 168 deletions

View file

@ -0,0 +1,80 @@
#-*- coding: utf-8 -*-
## @package DataSource.MySQL.fieldtypes
#
# Defines usefull function to handle fieldtype in MySQL datasource
import EditorialModel
from EditorialModel import fieldtypes as ed_fieldtypes
import EditorialModel.fieldtypes.generic
from EditorialModel.fieldtypes.generic import SingleValueFieldType, MultiValueFieldType, ReferenceFieldType
import EditorialModel.fieldtypes.integer
import EditorialModel.fieldtypes.char
import EditorialModel.fieldtypes.bool
import EditorialModel.fieldtypes.text
import EditorialModel.fieldtypes.rel2type
## @brief Returns column specs from fieldtype
# @param emfieldtype EmFieldType : An EmFieldType insance
# @todo escape default value
def singlevaluefieldtype_db_init_specs(emfieldtype):
colspec = ''
if not emfieldtype.nullable:
colspec = 'NOT NULL'
if hasattr(emfieldtype, 'default'):
colspec += ' DEFAULT '
if emfieldtype.default is None:
colspec += 'NULL '
else:
colspec += emfieldtype.default # ESCAPE VALUE HERE !!!!
if emfieldtype.name == 'pk':
colspec += ' AUTO_INCREMENT'
return colspec
## @brief Given a fieldtype return instructions to be executed by the migration handler
#
# The returned value in a tuple of len = 3
#
# The first items gives instruction type. Possible values are :
# - 'column' : add a column
# - the second tuple item is the SQL type of the new column
# - the third tuple item is the SQL specs (constraints like default value, nullable, unique , auto_increment etc.)
# - 'table' : add a column in another table and make a fk to the current table
# - the second tuple item is a tuple(key_name, key_value)
# - the third tuple item is a tuple(column_type, column_spec)
# @param fieldtype GenericFieldType : A FieldType instance
# @return a tuple (instruction_type, infos)
def fieldtype_db_init(fieldtype):
if isinstance(fieldtype, EditorialModel.fieldtypes.rel2type.EmFieldType):
return (None, None, None)
elif isinstance(fieldtype, SingleValueFieldType):
res = [ 'column', None, singlevaluefieldtype_db_init_specs(fieldtype) ]
# We will create a column
if isinstance(fieldtype, EditorialModel.fieldtypes.integer.EmFieldType):
res[1] = 'INT'
elif isinstance(fieldtype, EditorialModel.fieldtypes.char.EmFieldType):
res[1] = 'VARCHAR(%d)' % fieldtype.max_length
elif isinstance(fieldtype, EditorialModel.fieldtypes.text.EmFieldType):
res[1] = 'TEXT'
elif isinstance(fieldtype, EditorialModel.fieldtypes.bool.EmFieldType):
res[1] = 'BOOL'
elif isinstance(fieldtype, EditorialModel.fieldtypes.datetime.EmFieldType):
res[1] = 'DATETIME'
elif isinstance(fieldtype, EditorialModel.fieldtypes.generic.ReferenceFieldType):
res[1] = 'INT'
else:
raise RuntimeError("Unsupported fieldtype : ", fieldtype)
return tuple(res)
elif isinstance(fieldtype, MultiValueFieldType):
res = [ 'table', None, None ]
key_type, _ = fieldtype_db_init(fieldtype.key_fieldtype)
key_name = fieldtype.keyname
res[1] = (key_name, key_type)
res[2] = fieldtype_db_init(fieldtype.value_fieldtype)
else:
raise NotImplementedError("Not yet implemented")

View file

@ -10,6 +10,7 @@ import EditorialModel.classtypes
import EditorialModel.fieldtypes
import EditorialModel.fieldtypes.generic
from DataSource.MySQL import fieldtypes as fieldtypes_utils
from DataSource.MySQL import utils
from DataSource.dummy.migrationhandler import DummyMigrationHandler
@ -274,8 +275,6 @@ class MysqlMigrationHandler(DummyMigrationHandler):
# Creating foreign keys between relation and object table
sup_cname, sub_cname = self.get_sup_and_sub_cols()
self._add_fk(tname, object_tname, sup_cname, self._object_pk[0], 'fk_relations_superiors')
self._add_fk(tname, object_tname, sub_cname, self._object_pk[0], 'fk_relations_subordinate')
## @brief Returns the fieldname for superior and subordinate in relation table
# @return a tuple (superior_name, subordinate_name)
@ -301,8 +300,9 @@ class MysqlMigrationHandler(DummyMigrationHandler):
def _create_table(self, table_name, pk_name, pk_ftype, engine, charset='utf8', if_exists='nothing'):
#Escaped table name
etname = utils.escape_idname(table_name)
pk_type = self._field_to_type(pk_ftype)
pk_specs = self._field_to_specs(pk_ftype)
instr_type, pk_type, pk_specs = fieldtypes_utils.fieldtype_db_init(pk_ftype)
if instr_type != 'column':
raise ValueError("Migration handler doesn't support MultiValueFieldType as primary keys")
if if_exists == 'drop':
self._query("""DROP TABLE IF EXISTS {table_name};""".format(table_name=etname))
@ -343,11 +343,17 @@ ADD COLUMN {col_name} {col_type} {col_specs};"""
etname = utils.escape_idname(table_name)
ecname = utils.escape_idname(col_name)
instr, col_type, col_specs = fieldtypes_utils.fieldtype_db_init(col_fieldtype)
if instr is None:
return True
if instr != "column":
raise RuntimeError("Bad implementation")
add_col = add_col.format(
table_name=etname,
col_name=ecname,
col_type=self._field_to_type(col_fieldtype),
col_specs=self._field_to_specs(col_fieldtype),
col_type=col_type,
col_specs=col_specs,
)
try:
self._query(add_col)
@ -359,6 +365,31 @@ ADD COLUMN {col_name} {col_type} {col_specs};"""
#LOG
print("Aborded, column `%s` exists" % col_name)
return False
if isinstance(col_fieldtype, EditorialModel.fieldtypes.generic.ReferenceFieldType):
# We have to create a FK !
if col_fieldtype.reference == 'object':
dst_table_name = utils.common_tables['object']
dst_col_name, _ = self._object_pk
elif col_fieldtypes.reference == 'relation':
dst_table_name = utils.common_tables['relation']
dst_col_name, _ = self._relation_pk
fk_name = 'fk_%s-%s_%s-%s' % (
table_name,
col_name,
dst_table_name,
dst_col_name,
)
self._add_fk(
src_table_name = table_name,
dst_table_name = dst_table_name,
src_col_name = col_name,
dst_col_name = dst_col_name,
fk_name = fk_name
)
return True
## @brief Add a foreign key
@ -398,7 +429,7 @@ FOREIGN KEY ({src_col}) references {dst_table}({dst_col});""".format(
self._query("""ALTER TABLE {src_table}
DROP FOREIGN KEY {fk_name}""".format(
src_table=utils.escape_idname(src_table_name),
fk_name=fk_name
fk_name=utils.escape_idname(fk_name)
))
except self._dbmodule.err.InternalError:
# If the FK don't exists we do not care
@ -412,7 +443,7 @@ DROP FOREIGN KEY {fk_name}""".format(
colval_l_ins = dict() # param for insert trigger
for cname, cftype in cols_ftype.items():
if cftype.ftype == 'datetime':
if isinstance(cftype, EditorialModel.fieldtypes.datetime.EmFieldType):
if cftype.now_on_update:
colval_l_upd[cname] = 'NOW()'
if cftype.now_on_create:
@ -450,65 +481,6 @@ FOR EACH ROW SET {col_val_list};""".format(
)
self._query(trig_q)
## @brief Identifier escaping
# @param idname str : An SQL identifier
#def _idname_escape(self, idname):
# if '`' in idname:
# raise ValueError("Invalid name : '%s'"%idname)
# return '`%s`'%idname
## @brief Returns column specs from fieldtype
# @param emfieldtype EmFieldType : An EmFieldType insance
# @todo escape default value
def _field_to_specs(self, emfieldtype):
colspec = ''
if not emfieldtype.nullable:
colspec = 'NOT NULL'
if hasattr(emfieldtype, 'default'):
colspec += ' DEFAULT '
if emfieldtype.default is None:
colspec += 'NULL '
else:
colspec += emfieldtype.default # ESCAPE VALUE HERE !!!!
if emfieldtype.name == 'pk':
colspec += ' AUTO_INCREMENT'
return colspec
## @brief Given a fieldtype return a MySQL type specifier
# @param emfieldtype EmFieldType : A fieldtype
# @return the corresponding MySQL type
def _field_to_type(self, emfieldtype):
ftype = emfieldtype.ftype
if ftype == 'char' or ftype == 'str':
res = "VARCHAR(%d)" % emfieldtype.max_length
elif ftype == 'text':
res = "TEXT"
elif ftype == 'datetime':
res = "DATETIME"
# client side workaround for only one column with CURRENT_TIMESTAMP : giving NULL to timestamp that don't allows NULL
# cf. https://dev.mysql.com/doc/refman/5.0/en/timestamp-initialization.html#idm139961275230400
# The solution for the migration handler is to create triggers :
# CREATE TRIGGER trigger_name BEFORE INSERT ON `my_super_table`
# FOR EACH ROW SET NEW.my_date_column = NOW();
# and
# CREATE TRIGGER trigger_name BEFORE UPDATE ON
elif ftype == 'bool':
res = "BOOL"
elif ftype == 'int':
res = "INT"
elif ftype == 'rel2type':
res = "INT"
elif ftype == 'leobject':
res = "INT"
else:
raise ValueError("Unsuported fieldtype ftype : %s" % ftype)
return res
## @brief Delete all table created by the MH
# @param model Model : the Editorial model
def __purge_db(self, model):

View file

@ -1,17 +1,15 @@
#-*- coding: utf-8 -*-
from .generic import GenericFieldType
from .generic import SingleValueFieldType
class EmFieldType(GenericFieldType):
class EmFieldType(SingleValueFieldType):
help = 'Basic string (varchar) field. Take max_length=64 as option'
ftype = 'char'
## @brief A char field
# @brief max_length int : The maximum length of this field
def __init__(self, max_length=64, **kwargs):
self.max_length = max_length
super(EmFieldType, self).__init__(ftype='char', **kwargs)
super(EmFieldType, self).__init__(**kwargs)

View file

@ -1,14 +1,12 @@
#-*- coding: utf-8 -*-
from .generic import GenericFieldType
from .generic import SingleValueFieldType
class EmFieldType(GenericFieldType):
class EmFieldType(SingleValueFieldType):
help = 'A datetime field. Take two boolean options now_on_update and now_on_create'
ftype = 'datetime'
## @brief A datetime field
# @param now_on_update bool : If true the date is set to NOW on update
# @param now_on_create bool : If true the date is set to NEW on creation
@ -16,4 +14,4 @@ class EmFieldType(GenericFieldType):
def __init__(self, now_on_update=False, now_on_create=False, **kwargs):
self.now_on_update = now_on_update
self.now_on_create = now_on_create
super(EmFieldType, self).__init__(ftype='datetime', **kwargs)
super(EmFieldType, self).__init__(**kwargs)

View file

@ -13,7 +13,7 @@ class EmFieldType(integer.EmFieldType):
def __init__(self, is_id_class, **kwargs):
self._is_id_class = is_id_class
kwargs['internal'] = 'automatic'
super(EmFieldType, self).__init__(is_id_class = is_id_class, **kwargs)
super().__init__(is_id_class = is_id_class, **kwargs)
def _check_data_value(self, value):
return (value, None)

View file

@ -3,80 +3,44 @@
import types
import importlib
## @brief Abstract class representing a fieldtype
#
# Important notes on datas checking :
class GenericFieldType(object):
help_text = 'Generic field type : abstract class for every fieldtype'
## @brief Text describing the fieldtype
help = 'Generic field type : abstract class for every fieldtype'
## @brief Allowed type for handled datas
_allowed_ftype = ['char', 'str', 'int', 'bool', 'datetime', 'text', 'rel2type', 'leobject']
## @brief The basic lowlevel value type
ftype = None
## @brief Instanciate a new fieldtype
# @param ftype str : The type of datas handled by this fieldtype
# @param nullable bool : is None allowed as value ?
# @param check_function function : A callback check function that takes 1 argument and raise a TypeError if the validation fails
# @param uniq bool : Indicate if a field should handle uniq values
# @param primary bool : If true the field is a primary key
# @param **kwargs dict : Other arguments
# @throw NotImplementedError if called directly
# @throw AttributeError if bad ftype
# @throw AttributeError if bad check_function
def __init__(self, ftype, nullable=True, check_data_value=None, uniq=False, primary=False, **kwargs):
## @param internal False | str : define wheter or not a field is internal
# @throw NotImplementedError if called from bad class
def __init__(self, internal = False, **args):
if self.__class__ == GenericFieldType:
raise NotImplementedError("Abstract class")
if self.ftype is None:
raise RuntimeError("The class attribute ftype is not properly set by the %s EmFieldType" % self.name)
if ftype not in self._allowed_ftype:
raise AttributeError("Ftype '%s' not known" % ftype)
if ftype != self.__class__.ftype:
raise RuntimeError("The ftype is not the same for the instance and the class. Maybe %s reimplement ftype at class level but shouldn't" % self.name)
self.ftype = ftype
self.nullable = bool(nullable)
self.uniq = bool(uniq)
if 'default' in kwargs:
self.default, error = self.check_data_value(kwargs['default'])
if error:
raise error
del kwargs['default']
for argname, argvalue in kwargs.items():
setattr(self, argname, argvalue)
## @return A fieldtype name from an instance
self.internal = internal #Check this value ?
for argname, argval in args.items():
setattr(self, argname, argval)
## Fieldtype name
# @todo should be a staticmethod
@property
def name(self):
return self.__module__.split('.')[-1]
## @return True if a field is internal, else returns False
## @return True if a fieldtype is internal
def is_internal(self):
return hasattr(self, 'internal') and self.internal
## @brief Check if a Value is correct else return a check fail reason
# @param value * : The value to check
# @return (checked_and_casted_value, Exception|None)
def check_data_value(self, value):
if value is None:
if not self.nullable:
return (None, TypeError("'None' value but field is not nullable"))
return (None, None)
return self._check_data_value(value)
return self.internal != False
## @brief Build automatic fields values
## @brief Take care to call the fieldtype defined _check_data_value() method
# @return a tuple (value, error|None)
def check_data_value(self, value):
return self._check_data_value(value)
def _check_data_value(self, value):
return (value, None)
## @brief Build field value
# @param lec LeCrud : A LeCrud child class
# @param fname str : The field name
# @param datas dict : dict storing fields values
# @return constructed datas
# @param fname str : The field name
# @param datas dict : dict storing fields values (from the lec)
# @return constructed datas (for the fname field)
# @throw RuntimeError if unable to construct data
def construct_data(self, lec, fname, datas):
if fname in datas:
return datas[fname]
@ -85,7 +49,7 @@ class GenericFieldType(object):
elif lec.fieldtypes()[fname].nullable:
return None
raise RuntimeError("Unable to construct data for field %s", fname)
## @brief Check datas consistency
# @param leo LeCrud : A LeCrud child class instance
# @param fname str : The field name
@ -94,12 +58,6 @@ class GenericFieldType(object):
def check_data_consistency(self, lec, fname, datas):
return True
## @brief Dummy check, designed to be implemented in child classes
# @param value * : The value
# @return (checked_and_casted_value, Exception|None)
def _check_data_value(self, value):
return (value, None)
## @brief Given a fieldtype name return the associated python class
# @param fieldtype_name str : A fieldtype name
# @return An GenericFieldType derivated class
@ -122,18 +80,83 @@ class GenericFieldType(object):
hash_dats.append((kdic, getattr(self, kdic)))
return hash(tuple(hash_dats))
## @brief Transform a value into a valid python representation according to the fieldtype
# @param value ? : The value to cast
# @param kwargs dict : optionnal cast arguments
# @return Something (depending on the fieldtype
# @throw AttributeError if error in argument given to the method
# @throw TypeError if the cast is not possible
def cast(self, value, **kwargs):
if len(kwargs) > 0:
raise AttributeError("No optionnal argument allowed for %s cast method" % self.__class__.__name__)
return value
class SingleValueFieldType(GenericFieldType):
## @brief Instanciate a new fieldtype
# @param nullable bool : is None allowed as value ?
# @param uniqu bool : Indicate if a field should handle uniq value
# @param primary bool : If True the field is a primary key
# @param **args : Other arguments
# @throw NotImplementedError if called from bad class
def __init__(self, internal = False, nullable = True, uniq = False, primary = False, **args):
if self.__class__ == SingleValueFieldType:
raise NotImplementedError("Asbtract class")
super().__init__(internal, **args)
self.nullable = nullable
self.uniq = uniq
self.primary = primary
if 'default' in args:
self.default, error = self.check_data_value(args['default'])
if error:
raise error
del(args['default'])
def check_data_value(self, value):
if value is None:
if not self.nullable:
return (None, TypeError("'None' value but field is not nullable"))
return (None, None)
return super().check_data_value(value)
class ReferenceFieldType(SingleValueFieldType):
## @brief Instanciate a new fieldtype
#
#
# @param reference str : A string that defines the reference (can be 'object' or 'relation')
# @param nullable bool : is None allowed as value ?
# @param unique bool : Indicate if a field should handle uniq value
# @param primary bool : If True the field is a primary key
# @param **args : Other arguments
# @throw NotImplementedError if called from bad class
def __init__(self, reference, internal=False, nullable = True, uniq = False, primary = False, **args):
if reference.lower() not in ['relation', 'object']:
raise ValueError("Bad value for reference : %s. Excepted on of 'relation', 'object'" % reference)
self.reference = reference.lower()
super().__init__(
internal = internal,
nullable = nullable,
uniq = uniq,
primary = primary,
**args
);
pass
class MultiValueFieldType(GenericFieldType):
## @brief Instanciate a new multivalue fieldtype
#
# This fieldtype describe a field that handles multiple values referenced by a key (like a dict).
# A typicall example is the i18n fieldtype
# @param keyname str : The identifier key name
# @param key_fieldtype SingleValueFieldType : A SingleValueFieldType child class instance
# @param value_fieldtype SingleValueFieldType : A SingleValueFieldType child class instance
def __init__(self, internal, keyname, key_fieldtype, value_fieldtype, **args):
super().__init__(internal)
## stores the keyname
self.keyname = keyname
## stores the fieldtype that handles the key
self.key_fieldtype = key_fieldtype
## stores the fieldtype that handles the values
self.value_fieldtype = value_fieldtype
#
#
# Exceptions
#
#
class FieldTypeError(Exception):
pass

View file

@ -1,9 +1,9 @@
#-*- coding: utf-8 -*-
from .generic import GenericFieldType
from .generic import SingleValueFieldType
class EmFieldType(GenericFieldType):
class EmFieldType(SingleValueFieldType):
help = 'Basic integer field'

View file

@ -3,16 +3,17 @@
import leapi.letype as letype
import leapi.leclass as leclass
from .generic import GenericFieldType, FieldTypeError
from .generic import ReferenceFieldType, FieldTypeError
class EmFieldType(GenericFieldType):
class EmFieldType(ReferenceFieldType):
help = 'Fieldtypes designed to handle pk of LeObject in LeRelations'
ftype = 'leobject'
## @todo Replace hardcoded string for reference initialisation
def __init__(self, superior=True, **kwargs):
super(EmFieldType, self).__init__(ftype = 'leobject', superior = superior, **kwargs)
super().__init__(superior = superior, reference='object', **kwargs)
def _check_data_value(self, value):
if not isinstance(value, int):

View file

@ -10,10 +10,10 @@ class EmFieldType(EmFieldType):
help = 'Stores a relation\'s nature'
def __init__(self, **kwargs):
kwargs.update({'nullable': True, 'check_data_value': EmFieldType.check_data_value})
super(EmFieldType, self).__init__(**kwargs)
kwargs.update({'nullable': True})
super().__init__(**kwargs)
def check_data_value(self, value):
def _check_data_value(self, value):
if value is None or ( value in classtypes.EmNature.getall()):
return value, None
else:

View file

@ -12,7 +12,7 @@ class EmFieldType(GenericFieldType):
def __init__(self, rel_to_type_id, **kwargs):
self.rel_to_type_id = rel_to_type_id
super(EmFieldType, self).__init__(ftype='rel2type', **kwargs)
super(EmFieldType, self).__init__(**kwargs)
def get_related_type(self):
return self.model.component(self.rel_to_type_id)