1
0
Fork 0
mirror of https://github.com/yweber/lodel2.git synced 2025-11-14 18:09:17 +01:00

New version (again) of the fieldtypes

This commit is contained in:
Yann 2015-09-17 15:10:22 +02:00
commit 773afc5b2c
9 changed files with 139 additions and 505 deletions

3
.gitignore vendored
View file

@ -4,4 +4,5 @@
.idea
Lodel/settings/locale.py
Lodel/settings/local.py
doc
doc
.*.swp

View file

@ -1,5 +1,7 @@
#-*- coding: utf-8 -*-
import importlib
from EditorialModel.components import EmComponent
from EditorialModel.exceptions import EmComponentCheckError
import EditorialModel
@ -12,6 +14,8 @@ class EmField(EmComponent):
ranked_in = 'fieldgroup_id'
ftype = None
fieldtypes = {
'int': models.IntegerField,
'integer': models.IntegerField,
@ -30,24 +34,44 @@ class EmField(EmComponent):
## Instanciate a new EmField
# @todo define and test type for icon and fieldtype
def __init__(self, model, uid, name, fieldgroup_id, fieldtype, optional=False, internal=False, rel_to_type_id=None, rel_field_id=None, icon='0', string=None, help_text=None, date_update=None, date_create=None, rank=None, **kwargs):
# @warning nullable == True by default
def __init__(self, model, uid, name, fieldgroup_id, optional=False, internal=False, rel_field_id=None, icon='0', string=None, help_text=None, date_update=None, date_create=None, rank=None, nullable = True, default = None, **kwargs):
self.fieldgroup_id = fieldgroup_id
self.check_type('fieldgroup_id', int)
self.fieldtype = fieldtype
self.optional = optional
self.check_type('optional', bool)
self.internal = internal
self.check_type('internal', bool)
self.rel_to_type_id = rel_to_type_id
self.check_type('rel_to_type_id', (int, type(None)))
self.rel_field_id = rel_field_id
self.check_type('rel_field_id', (int, type(None)))
self.icon = icon
self.nullable = nullable
self.default = default
self.options = kwargs
super(EmField, self).__init__(model=model, uid=uid, name=name, string=string, help_text=help_text, date_update=date_update, date_create=date_create, rank=rank)
@staticmethod
def get_field_class(ftype, **kwargs):
ftype_module = importlib.import_module('EditorialModel.fieldtypes.%s'%ftype)
return ftype_module.fclass
## @brief Abstract method that should return a validation function
# @param raise_e Exception : if not valid raise this exception
# @param ret_valid : if valid return this value
# @param ret_invalid : if not valid return this value
def validation_function(self, raise_e = None, ret_valid = None, ret_invalid = None):
if self.__class__ == EmField:
raise NotImplementedError("Abstract method")
if raise_e is None and ret_valid is None:
raise AttributeError("Behavior doesn't allows to return a valid validation function")
return False
## @brief Return the list of relation fields for a rel_to_type
# @return None if the field is not a rel_to_type else return a list of EmField
def rel_to_type_fields(self):
@ -72,6 +96,7 @@ class EmField(EmComponent):
def delete_check(self):
return True
"""
def to_django(self):
if self.fieldtype in ('varchar', 'char'):
max_length = None if 'max_length' not in self.options else self.options['max_length']
@ -86,3 +111,4 @@ class EmField(EmComponent):
return models.NullBooleanField(**self.options)
return self.fieldtypes[self.fieldtype](**self.options)
"""

View file

@ -1,494 +0,0 @@
#-*- coding: utf-8 -*-
from django.db import models
import re
import datetime
import json
import importlib
import copy
import EditorialModel
from Lodel.utils.mlstring import MlString
## @brief Characterise fields for LeObject and EmComponent
# This class handles values rules for LeObject and EmComponents.
#
# It allows to EmFieldValue classes family to have rules to cast, validate and save values.
#
# There is exposed methods that allows LeObject and EmType to run a save sequence. This methods are :
# - EmFieldType.to_value() That cast a value to an EmFieldType internal storage
# - EmFieldType.is_valid() That indicates wether or not a value is valid
# - EmFieldType.pre_save() That returns weighted SQL request that must be run before any value save @ref EditorialModel::types::EmType.save_values()
# - EmFieldType.to_sql() That returns a value that can be inserted in database.
# - EmFieldType.post_save() That returns weighted SQL request that must be run after any value save @ref EditorialModel::types::EmType.save_values()
#
class EmFieldType(object):
## Stores options and default value for EmFieldType
# options list :
# - type (str|None) : if None will make an 'abstract' fieldtype (with no value assignement possible)
# - nullable (bool) : tell whether or not a fieldType accept NULL value
# - default (mixed) : The default value for a FieldType
# - primarykey (bool) : If true the fieldType represent a PK
# - autoincrement (bool) : If true the fieldType will be autoincremented
# - index (bool) : If true the columns will be indexed in Db
# - name (str) : FieldType name
# - doc (str) : FieldType documentation
# - onupdate (callable) : A callback to call on_update
# - valueobject (EmFieldValue or childs) : An object that represent values for this fieldType
# - type_* (mixed) : Use to construct an options dictinnary for a type (exemple : type_length => nnewColumn(lenght= [type_lenght VALUE] )) @ref EmFieldSQLType
# - validators (list) : List of validator functions to use
_opt = {
'name': None,
'type': None,
'nullable': True,
'default': None,
'primarykey': False,
'autoincrement': False,
'uniq': False,
'index': False,
'doc': None,
'onupdate': None,
'valueobject': None,
'validators': None
}
## Instanciate an EmFieldType
# For arguments see @ref EmFieldType::_opt
# @see EmFieldType::_opt
def __init__(self, **kwargs):
self.__init(kwargs)
self.type_options = dict()
# stores all col_* arguments into the self.type_options dictionary
args = kwargs.copy()
for optname in args:
type_opt = re.sub(r'^type_', '', optname)
if type_opt != optname:
self.type_options[type_opt] = args[optname]
del kwargs[optname]
# checks if the other arguments are valid
if len(set(kwargs.keys()) - set(self.__class__._opt.keys())) > 0:
badargs = ""
for bad in set(kwargs.keys()) - set(self.__class__._opt.keys()):
badargs += " " + bad
raise TypeError("Unexpected arguments : %s" % badargs)
# stores other arguments as instance attribute
for opt_name in self.__class__._opt:
setattr(self, opt_name, (kwargs[opt_name] if opt_name in kwargs else self.__class__._opt[opt_name]))
# checks type's options valididty
if self.type != None:
try:
EmFieldSQLType.sqlType(self.type, **self.type_options)
except TypeError as e:
raise e
# Default value for name
if self.name == None:
if self.__class__ == EmFieldType:
self.name = 'generic'
else:
self.name = self.__class__.__name__
# Default value for doc
if self.doc == None:
if self.__class__ == EmFieldType:
self.doc = 'Abstract generic EmFieldType'
else:
self.doc = self.__class__.__name__
## MUST be called first in each constructor
# @todo this solution is not good (look at the __init__ for EmFieldType childs)
def __init(self, kwargs):
try:
self.args_copy
except AttributeError:
self.args_copy = kwargs.copy()
@property
## A predicate that indicates whether or not an EmFieldType is "abstract"
def is_abstract(self):
return (self.type == None)
## Returns a copy of the current EmFieldType
def copy(self):
args = self.args_copy.copy()
return self.__class__(**args)
def dump_opt(self):
return json.dumps(self.args_copy)
@staticmethod
## Return an instance from a classname and options from dump_opt
# @param classname str: The EmFieldType class name
# @param json_opt str: getted from dump_opt
def restore(colname, classname, json_opt):
field_type_class = getattr(EditorialModel.fieldtypes, classname)
init_opt = json.loads(json_opt)
init_opt['name'] = colname
return field_type_class(**init_opt)
## Return a value object 'driven' by this EmFieldType
# @param name str: The column name associated with the value
# @param *init_val mixed: If given this will be the initialisation value
# @return a EmFieldValue instance
# @todo better default value (and bad values) handling
def valueObject(self, name, *init_val):
if self.valueObject == None:
return EmFieldValue(name, self, *init_val)
return self.valueObject(name, self, *init_val)
## Cast to the correct value
# @param v mixed : the value to cast
# @return A gently casted value
# @throw ValueError if v is innapropriate
# @throw NotImplementedError if self is an abstract EmFieldType
def to_value(self, v):
if self.type == None:
raise NotImplemented("This EmFieldType is abstract")
if v == None and not self.nullable:
raise TypeError("Field not nullable")
return v
## to_value alias
# @param value mixed : the value to cast
# @return A casted value
def from_string(self, value):
return self.to_value(value)
## Returns a gently sql forged value
# @return A sql forged value
# @warning It assumes that the value is correct and comes from an EmFieldValue object
def to_sql(self, value):
if self.is_abstract:
raise NotImplementedError("This EmFieldType is abstract")
return True
## Returns whether or not a value is valid for this EmFieldType
# @note This function always returns True and is here for being overloaded by child objects
# @return A boolean, True if valid else False
def is_valid(self, value):
if self.is_abstract:
raise NotImplementedError("This EmFieldType is abstract")
return True
## Pre-save actions
def pre_save(self):
if self.is_abstract:
raise NotImplementedError("This EmFieldType is abstract")
return []
## Post-save actions
def post_save(self):
if self.is_abstract:
raise NotImplementedError("This EmFieldType is abstract")
return []
@classmethod
## Function designed to be called by child class to enforce a type
# @param args dict: The kwargs argument of __init__
# @param typename str: The typename to enforce
# @return The new kwargs to be used
# @throw TypeError if type is present is args
def _setType(cl, args, typename):
return cl._argEnforce(args, 'type', typename, True)
@classmethod
## Function designed to be called by child's constructo to enforce an argument
# @param args dict: The constructor's kwargs
# @param argname str: The name of the argument to enforce
# @param argval mixed: The value we want to enforce
# @param Return a new kwargs
# @throw TypeError if type is present is args and exception argument is True
def _argEnforce(cl, args, argname, argval, exception=True):
if exception and argname in args:
raise TypeError("Invalid argument '"+argname+"' for "+cl.__class__.__name__+" __init__")
args[argname] = argval
return args
@classmethod
## Function designed to be called by child's constructor to set a default value
# @param args dict: The constructor's kwargs
# @param argname str: The name of the argument with default value
# @param argval mixed : The default value
# @return a new kwargs dict
def _argDefault(cl, args, argname, argval):
if argname not in args:
args[argname] = argval
return args
class EmFieldValue(object):
## Instanciates a EmFieldValue
# @param name str : The column name associated with this value
# @param fieldtype EmFieldType: The EmFieldType defining the value
# @param *value *list: This argument allow to pass a value to set (even None) and to detect if no value given to set to EmFieldType's default value
# @throw TypeError if more than 2 arguments given
# @throw TypeError if fieldtype is not an EmFieldType
# @throw TypeError if fieldtype is an abstract EmFieldType
def __init__(self, name, fieldtype, *value):
if not isinstance(fieldtype, EmFieldType):
raise TypeError("Expected <class EmFieldType> for 'fieldtype' argument, but got : %s instead" % str(type(fieldtype)))
if fieldtype.is_abstract:
raise TypeError("The given fieldtype in argument is abstract.")
# This copy ensures that the fieldtype will not change during the value lifecycle
super(EmFieldValue, self).__setattr__('fieldtype', fieldtype.copy())
if len(value) > 1:
raise TypeError("Accept only 2 positionnal parameters. %s given." % str(len(value)+1))
elif len(value) == 1:
self.value = value[0]
else:
self.value = fieldtype.default
# Use this to set value in the constructor
setv = super(EmFieldValue, self).__setattr__
# This copy makes column attributes accessible easily
for attrname in self.fieldtype.__dict__:
setv(attrname, getattr(self.fieldtype, attrname))
# Assign some EmFieldType methods to the value
setv('from_python', self.fieldtype.to_value)
setv('from_string', self.fieldtype.to_value)
#setv('sqlCol', self.fieldtype.sqlCol)
## The only writable attribute of EmFieldValue is the value
# @param name str: Have to be value
# @param value mixed: The value to set
# @throw AtrributeError if another attribute than value is to be set
# @throw ValueError if self.to_value raises it
# @see EmFieldType::to_value()
def __setattr__(self, name, value):
if name != "value":
raise AttributeError("EmFieldValue has only the value property settable")
super(EmFieldValue,self).__setattr__('value', self.fieldtype.to_value(value))
##
# @warning When getting the fieldtype you actually get a copy of it to prevent any modifications !
def __getattr__(self, name):
if name == 'fieldtype':
return self.fieldtype.copy()
return super(EmFieldValue, self).__getattribute__(name)
## @brief Return a valid SQL value
#
# Can be used to convert any value (giving one positionnal argument) or to return the current value
# @param *value list: If a positionnal argument is given return it and not the instance value
# @return A value suitable for sql
def to_sql(self, *value):
if len(value) > 1:
raise TypeError("Excepted 0 or 1 positional argument but got "+str(len(value)))
elif len(value) == 1:
return self.fieldtype.to_sql(value[0])
return self.fieldtype.to_sql(self.value)
class EmFieldSQLType(object):
_integer = {'sql': models.IntegerField}
_bigint = {'sql': models.BigIntegerField}
_smallint = {'sql': models.SmallIntegerField}
_boolean = {'sql': models.BooleanField}
_nullableboolean = {'sql': models.NullBooleanField}
_float = {'sql': models.FloatField}
_varchar = {'sql': models.CharField}
_text = {'sql': models.TextField}
_time = {'sql': models.TimeField}
_date = {'sql': models.DateField}
_datetime = {'sql': models.DateTimeField}
_names = {
'int': _integer,
'integer': _integer,
'bigint': _bigint,
'smallint': _smallint,
'boolean': _boolean,
'bool': _boolean,
'float': _float,
'char': _varchar,
'varchar': _varchar,
'text': _text,
'time': _time,
'date': _date,
'datetime': _datetime,
}
@classmethod
def sqlType(cls, name, **kwargs):
if not isinstance(name, str):
raise TypeError("Expect <class str>, <class int>|None but got : %s %s" % (str(type(name)), str(type(size))))
name = name.lower()
if name not in cls._names:
raise ValueError("Unknown type '%s'" % name)
if name in ['boolean','bool'] and kwargs['nullable'] in [1,'true']:
sqlclass = _nullableboolean
else:
sqlclass = cls._names[name]
if len(kwargs) == 0:
return sqlclass['sql']
return sqlclass['sql'](**kwargs)
## @brief Represents values with common arithmetic operations
class EmFieldValue_int(EmFieldValue):
def __int__(self):
return self.value
def __add__(self, other):
return self.value + other
def __sub__(self, other):
return self.value - other
def __mul__(self, other):
return self.value * other
def __div__(self, other):
return self.value / other
def __mod__(self, other):
return self.value % other
def __iadd__(self, other):
self.value = int(self.value + other)
return self
def __isub__(self, other):
self.value = int(self.value - other)
return self
def __imul__(self, other):
self.value = int(self.value * other)
return self
def __idiv__(self, other):
self.value = int(self.value / other)
return self
## @brief Handles integer fields
# @note Enforcing type to be int
# @note Default name is 'integer' and default 'valueobject' is EmFieldValue_int
class EmField_integer(EmFieldType):
def __init__(self, **kwargs):
self._init(kwargs)
# Default name
kwargs = self.__class__._argDefault(kwargs, 'name', 'integer')
# Default value object
kwargs = self.__class__._argDefault(kwargs, 'valueobject', EmFieldValue_int)
# Type enforcing
kwargs = self.__class__._setType(kwargs, 'int')
super(EmField_integer, self).__init__(**kwargs)
##
# @todo catch cast error ?
#def to_sql(self, value):
# return value
def to_value(self, value):
if value == None:
return super(EmField_integer, self).to_value(value)
return int(value)
## @brief Handles boolean fields
# @note Enforce type to be 'boolean'
# @note Default name is 'boolean'
class EmField_boolean(EmFieldType):
def __init__(self, **kwargs):
self._init(kwargs)
#Default name
kwargs = self.__class__._argDefault(kwargs, 'name', 'boolean')
#Type enforcing
kwargs = self.__class__._setType(kwargs, 'boolean')
super(EmField_boolean, self).__init__(**kwargs)
#def to_sql(self, value):
# return 1 if super(EmField_boolean, self).to_sql(value) else 0
def to_value(self, value):
if value == None:
return super(EmField_boolean, self).to_value(value)
self.value = bool(value)
return self.value
## @brief Handles string fields
# @note Enforce type to be (varchar)
# @note Default 'name' is 'char'
# @note Default 'type_length' is 76
class EmField_char(EmFieldType):
default_length = 76
def __init__(self, **kwargs):
self._init(kwargs)
kwargs = self.__class__._argDefault(kwargs, 'type_length', self.__class__.default_length)
kwargs = self.__class__._argDefault(kwargs, 'name', 'char')
#Type enforcing
kwargs = self.__class__._setType(kwargs, 'varchar')
super(EmField_char, self).__init__(**kwargs)
#def to_sql(self, value):
# return str(value)
## @brief Handles date fields
# @note Enforce type to be 'datetime'
# @todo rename to EmField_datetime
# @todo timezones support
class EmField_date(EmFieldType):
def __init__(self, **kwargs):
self._init(kwargs)
kwargs = self.__class__._argDefault(kwargs, 'name', 'date')
#Type enforcing
kwargs = self.__class__._setType(kwargs, 'datetime')
super(EmField_date, self).__init__(**kwargs)
#def to_sql(self, value):
# return value #thanks to sqlalchemy
def to_value(self, value):
if value == None:
return super(EmField_date, self).to_value(value)
if isinstance(value, int):
#assume its a timestamp
return datetime.fromtimestamp(value)
if isinstance(value, datetime.datetime):
return value
## @brief Handles strings with translations
class EmField_mlstring(EmField_char):
def __init__(self, **kwargs):
self._init(kwargs)
kwargs = self.__class__._argDefault(kwargs, 'name', 'mlstr')
super(EmField_mlstring, self).__init__(**kwargs)
#def to_sql(self, value):
# return value.__str__()
def to_value(self, value):
if value == None:
return super(EmField_mlstring, self).to_value(value)
if isinstance(value, str):
return MlString.load(value)
elif isinstance(value, MlString):
return value
raise TypeError("<class str> or <class MlString> excepted. But got "+str(type(value)))
## @brief Handles lodel uid fields
class EmField_uid(EmField_integer):
def __init__(self, **kwargs):
self._init(kwargs)
kwargs = self.__class__._argEnforce(kwargs, 'primarykey', True)
super(EmField_uid, self).__init__(**kwargs)

View file

@ -0,0 +1,5 @@
from os.path import dirname, basename, isfile
import glob
modules = glob.glob(dirname(__file__)+"/*.py")
__all__ = [ basename(f)[:-3] for f in modules if isfile(f) and f != '__init__.py']

View file

@ -0,0 +1,13 @@
#-*- coding: utf-8 -*-
from EditorialModel.fields import EmField
class EmFieldChar(EmField):
ftype = 'char'
def __init__(self, max_length=64, **kwargs):
self.max_length = max_length
super(EmFieldChar, self).__init__(**kwargs)
fclass = EmFieldChar

View file

@ -0,0 +1,13 @@
#-*- coding: utf-8 -*-
from EditorialModel.fields import EmField
class EmFieldInt(EmField):
ftype = 'int'
def __init__(self, **kwargs):
super(EmFieldChar, self).__init__(**kwargs)
fclass=EmFieldInt

View file

@ -0,0 +1,29 @@
#-*- coding: utf-8 -*-
import re
from EditorialModel.fieldtypes import EmFieldChar
class EmFieldCharRegex(EmFieldChar):
def __init__(self, regex = '', **kwargs):
self.regex = regex
v_re = re.compile(regex) #trigger an error if invalid regex
super(EmFieldCharRegex, self).__init__(**kwargs)
def validation_function(self, raise_e = None, ret_valid = None, ret_invalid = None):
super(EmFieldChar, self).validation_function(raise_e, ret_valid, ret_invalid)
if not raise_e is None:
def v_fun(value):
if not re.match(self.regex):
raise raise_e
else:
def v_fun(value):
if not re.match(self.regex):
return ret_invalid
else:
return ret_valid
return v_fun
fclass = EmFieldCharRegex

View file

@ -0,0 +1,20 @@
#-*- coding: utf-8 -*-
from EditorialModel.fields import EmField
class EmFieldRel2Type(EmField):
ftype= 'rel2type'
def __init__(self, rel_to_type_id, **kwargs):
self.rel_to_type_id = rel_to_type_id
super(EmFieldRel2Type, self).__init__(**kwargs)
def get_related_type(self):
return self.model.component(self.rel_to_type_id)
def get_related_fields(self):
return [ f for f in self.model.components(EmField) if f.rel_field_id == self.uid ]
fclass = EmFieldRel2Type

View file

@ -3,6 +3,7 @@
## @file editorialmodel.py
# Manage instance of an editorial model
import EditorialModel
from EditorialModel.migrationhandler.dummy import DummyMigrationHandler
from EditorialModel.classes import EmClass
from EditorialModel.fieldgroups import EmFieldGroup
@ -53,6 +54,9 @@ class Model(object):
# @return A class name as string or False if cls is not an EmComponent child class
def name_from_emclass(em_class):
if em_class not in Model.components_class:
spl = em_class.__module__.split('.')
if spl[1] == 'fieldtypes':
return 'EmField'
return False
return em_class.__name__
@ -67,11 +71,20 @@ class Model(object):
#Store and delete the EmComponent class name from datas
cls_name = kwargs['component']
del kwargs['component']
cls = self.emclass_from_name(cls_name)
if cls_name == 'EmField':
if not 'type' in kwargs:
raise AttributeError("Missing 'type' from EmField instanciation")
cls = EditorialModel.fields.EmField.get_field_class(kwargs['type'])
del(kwargs['type'])
else:
cls = self.emclass_from_name(cls_name)
if cls:
kwargs['uid'] = uid
# create a dict for the component and one indexed by uids, store instanciated component in it
self._components['uids'][uid] = cls(self, **kwargs)
self._components['uids'][uid] = cls(model=self, **kwargs)
self._components[cls_name].append(self._components['uids'][uid])
else:
raise ValueError("Unknow EmComponent class : '" + cls_name + "'")
@ -109,9 +122,10 @@ class Model(object):
## Sort components by rank in Model::_components
# @param emclass pythonClass : The type of components to sort
# @throw AttributeError if emclass is not valid
# @warning disabled the test on component_class because of EmField new way of working
def sort_components(self, component_class):
if component_class not in self.components_class:
raise AttributeError("Bad argument emclass : '" + component_class + "', excpeting one of " + str(self.components_class))
#if component_class not in self.components_class:
# raise AttributeError("Bad argument emclass : '" + str(component_class) + "', excpeting one of " + str(self.components_class))
self._components[self.name_from_emclass(component_class)] = sorted(self.components(component_class), key=lambda comp: comp.rank)
@ -129,9 +143,16 @@ class Model(object):
# @param datas dict : the options needed by the component creation
# @throw ValueError if datas['rank'] is not valid (too big or too small, not an integer nor 'last' or 'first' )
# @todo Handle a raise from the migration handler
# @todo Transform the datas arg in **datas ?
def create_component(self, component_type, datas):
em_obj = self.emclass_from_name(component_type)
if component_type == 'EmField':
if not 'type' in datas:
raise AttributeError("Missing 'type' from EmField instanciation")
em_obj = EditorialModel.fields.EmField.get_field_class(datas['type'])
del(datas['type'])
else:
em_obj = self.emclass_from_name(component_type)
rank = 'last'
if 'rank' in datas: