Browse Source

[#46] Changed the fieldtypes.py content to use Django field types instead of sqlalchemy's ones

Roland Haroutiounian 9 years ago
parent
commit
e63a4a8bdf
1 changed files with 439 additions and 136 deletions
  1. 439
    136
      EditorialModel/fieldtypes.py

+ 439
- 136
EditorialModel/fieldtypes.py View File

@@ -1,190 +1,493 @@
1 1
 #-*- coding: utf-8 -*-
2 2
 
3
-from Lodel.utils.mlstring import MlString
4
-import datetime
5 3
 from django.db import models
4
+import re
5
+import datetime
6
+import json
7
+import importlib
8
+import copy
9
+import EditorialModel
10
+from Lodel.utils.mlstring import MlString
6 11
 
7 12
 
8
-##
13
+## @brief Characterise fields for LeObject and EmComponent
14
+# This class handles values rules for LeObject and EmComponents.
9 15
 #
10
-# returns an instance of the corresponding EmFieldType class for a given name
16
+# It allows to EmFieldValue classes family to have rules to cast, validate and save values.
11 17
 #
12
-# @param name str: name of the field type
13
-# @param kwargs dict: options of the field type
14
-# @return EmFieldType
15
-def get_field_type(name):
16
-    class_name = 'EmField_'+name
17
-    constructor = globals()[class_name]
18
-    instance = constructor()
19
-    return instance
20
-
21
-
22
-##
18
+# There is exposed methods that allows LeObject and EmType to run a save sequence. This methods are :
19
+# - EmFieldType.to_value() That cast a value to an EmFieldType internal storage
20
+# - EmFieldType.is_valid() That indicates wether or not a value is valid
21
+# - EmFieldType.pre_save() That returns weighted SQL request that must be run before any value save @ref EditorialModel::types::EmType.save_values()
22
+# - EmFieldType.to_sql() That returns a value that can be inserted in database.
23
+# - EmFieldType.post_save() That returns weighted SQL request that must be run after any value save @ref EditorialModel::types::EmType.save_values()
23 24
 #
24
-# Generic Field type
25
-class EmFieldType():
26
-
25
+class EmFieldType(object):
26
+
27
+    ## Stores options and default value for EmFieldType
28
+    # options list :
29
+    # - type (str|None) : if None will make an 'abstract' fieldtype (with no value assignement possible)
30
+    # - nullable (bool) : tell whether or not a fieldType accept NULL value
31
+    # - default (mixed) : The default value for a FieldType
32
+    # - primarykey (bool) : If true the fieldType represent a PK
33
+    # - autoincrement (bool) : If true the fieldType will be autoincremented
34
+    # - index (bool) : If true the columns will be indexed in Db
35
+    # - name (str) : FieldType name
36
+    # - doc (str) : FieldType documentation
37
+    # - onupdate (callable) : A callback to call on_update
38
+    # - valueobject (EmFieldValue or childs) : An object that represent values for this fieldType
39
+    # - type_* (mixed) : Use to construct an options dictinnary for a type (exemple : type_length => nnewColumn(lenght= [type_lenght VALUE] )) @ref EmFieldSQLType
40
+    # - validators (list) : List of validator functions to use
41
+    _opt = {
42
+        'name': None,
43
+        'type': None,
44
+        'nullable': True,
45
+        'default': None,
46
+        'primarykey': False,
47
+        'autoincrement': False,
48
+        'uniq': False,
49
+        'index': False,
50
+        'doc': None,
51
+        'onupdate': None,
52
+        'valueobject': None,
53
+        'validators': None
54
+    }
55
+
56
+    ## Instanciate an EmFieldType
57
+    # For arguments see @ref EmFieldType::_opt
58
+    # @see EmFieldType::_opt
27 59
     def __init__(self, **kwargs):
28
-        self.name = kwargs['name'] #TODO gérer le cas où le name n'est pas passé
29
-        self.value = None
30
-        self.options = kwargs
31
-
32
-    def from_python(self, value):
33
-        self.value = value
34
-
35
-
36
-##
37
-#
38
-# Integer field type
39
-class EmField_integer(EmFieldType):
40
-
41
-    def __init__(self, **kwargs):
42
-        super(EmField_integer, self).__init__(**kwargs)
43
-
44
-    def to_django(self):
45
-        return models.IntegerField(**self.options)
46
-
47
-
48
-##
49
-#
50
-# Boolean field type
51
-class EmField_boolean(EmFieldType):
52
-
53
-    def __init__(self):
54
-        super(EmField_boolean, self).__init__(**kwargs)
55
-
56
-    def to_django(self):
57
-        if 'nullable' in self.options and self.options['nullable'] == 'true':
58
-            return models.NullBooleanField(**self.options)
60
+        self.__init(kwargs)
61
+        self.type_options = dict()
62
+
63
+        # stores all col_* arguments into the self.type_options dictionary
64
+        args = kwargs.copy()
65
+        for optname in args:
66
+            type_opt = re.sub(r'^type_', '', optname)
67
+            if type_opt != optname:
68
+                self.type_options[type_opt] = args[optname]
69
+                del kwargs[optname]
70
+
71
+        # checks if the other arguments are valid
72
+        if len(set(kwargs.keys()) - set(self.__class__._opt.keys())) > 0:
73
+            badargs = ""
74
+            for bad in set(kwargs.keys()) - set(self.__class__._opt.keys()):
75
+                badargs += " " + bad
76
+            raise TypeError("Unexpected arguments : %s" % badargs)
77
+
78
+        # stores other arguments as instance attribute
79
+        for opt_name in self.__class__._opt:
80
+            setattr(self, opt_name, (kwargs[opt_name] if opt_name in kwargs else self.__class__._opt[opt_name]))
81
+
82
+        # checks type's options valididty
83
+        if self.type != None:
84
+            try:
85
+                EmFieldSQLType.sqlType(self.type, **self.type_options)
86
+            except TypeError as e:
87
+                raise e
88
+
89
+        # Default value for name
90
+        if self.name == None:
91
+            if  self.__class__ == EmFieldType:
92
+                self.name = 'generic'
93
+            else:
94
+                self.name = self.__class__.__name__
95
+
96
+        # Default value for doc
97
+        if self.doc == None:
98
+            if self.__class__ == EmFieldType:
99
+                self.doc = 'Abstract generic EmFieldType'
100
+            else:
101
+                self.doc = self.__class__.__name__
102
+
103
+    ## MUST be called first in each constructor
104
+    # @todo this solution is not good (look at the __init__ for EmFieldType childs)
105
+    def __init(self, kwargs):
106
+        try:
107
+            self.args_copy
108
+        except AttributeError:
109
+            self.args_copy = kwargs.copy()
110
+
111
+    @property
112
+    ## A predicate that indicates whether or not an EmFieldType is "abstract"
113
+    def is_abstract(self):
114
+        return (self.type == None)
115
+
116
+    ## Returns a copy of the current EmFieldType
117
+    def copy(self):
118
+        args = self.args_copy.copy()
119
+        return self.__class__(**args)
120
+
121
+    def dump_opt(self):
122
+        return json.dumps(self.args_copy)
123
+
124
+    @staticmethod
125
+    ## Return an instance from a classname and options from dump_opt
126
+    # @param classname str: The EmFieldType class name
127
+    # @param json_opt str: getted from dump_opt
128
+    def restore(colname, classname, json_opt):
129
+        field_type_class = getattr(EditorialModel.fieldtypes, classname)
130
+        init_opt = json.loads(json_opt)
131
+        init_opt['name'] = colname
132
+        return field_type_class(**init_opt)
133
+
134
+    ## Return a value object 'driven' by this EmFieldType
135
+    # @param name str: The column name associated with the value
136
+    # @param *init_val mixed: If given this will be the initialisation value
137
+    # @return a EmFieldValue instance
138
+    # @todo better default value (and bad values) handling
139
+    def valueObject(self, name, *init_val):
140
+        if self.valueObject == None:
141
+            return EmFieldValue(name, self, *init_val)
142
+        return self.valueObject(name, self, *init_val)
143
+
144
+    ## Cast to the correct value
145
+    # @param v mixed : the value to cast
146
+    # @return A gently casted value
147
+    # @throw ValueError if v is innapropriate
148
+    # @throw NotImplementedError if self is an abstract EmFieldType
149
+    def to_value(self, v):
150
+        if self.type == None:
151
+            raise NotImplemented("This EmFieldType is abstract")
152
+        if v == None and not self.nullable:
153
+            raise TypeError("Field not nullable")
154
+        return v
155
+
156
+    ## to_value alias
157
+    # @param value mixed : the value to cast
158
+    # @return A casted value
159
+    def from_string(self, value):
160
+        return self.to_value(value)
161
+
162
+    ## Returns a gently sql forged value
163
+    # @return A sql forged value
164
+    # @warning It assumes that the value is correct and comes from an EmFieldValue object
165
+    def to_sql(self, value):
166
+        if self.is_abstract:
167
+            raise NotImplementedError("This EmFieldType is abstract")
168
+        return True
169
+
170
+    ## Returns whether or not a value is valid for this EmFieldType
171
+    # @note This function always returns True and is here for being overloaded  by child objects
172
+    # @return A boolean, True if valid else False
173
+    def is_valid(self, value):
174
+        if self.is_abstract:
175
+            raise NotImplementedError("This EmFieldType is abstract")
176
+        return True
177
+
178
+    ## Pre-save actions
179
+    def pre_save(self):
180
+        if self.is_abstract:
181
+            raise NotImplementedError("This EmFieldType is abstract")
182
+        return []
183
+
184
+    ## Post-save actions
185
+    def post_save(self):
186
+        if self.is_abstract:
187
+            raise NotImplementedError("This EmFieldType is abstract")
188
+        return []
189
+
190
+    @classmethod
191
+    ## Function designed to be called by child class to enforce a type
192
+    # @param args dict: The kwargs argument of __init__
193
+    # @param typename str: The typename to enforce
194
+    # @return The new kwargs to be used
195
+    # @throw TypeError if type is present is args
196
+    def _setType(cl, args, typename):
197
+        return cl._argEnforce(args, 'type', typename, True)
198
+
199
+    @classmethod
200
+    ## Function designed to be called by child's constructo to enforce an argument
201
+    # @param args dict: The constructor's kwargs
202
+    # @param argname str: The name of the argument to enforce
203
+    # @param argval mixed: The value we want to enforce
204
+    # @param Return a new kwargs
205
+    # @throw TypeError if type is present is args and exception argument is True
206
+    def _argEnforce(cl, args, argname, argval, exception=True):
207
+        if exception and argname in args:
208
+            raise TypeError("Invalid argument '"+argname+"' for "+cl.__class__.__name__+" __init__")
209
+        args[argname] = argval
210
+        return args
211
+
212
+    @classmethod
213
+    ## Function designed to be called by child's constructor to set a default value
214
+    # @param args dict: The constructor's kwargs
215
+    # @param argname str: The name of the argument with default value
216
+    # @param argval mixed : The default value
217
+    # @return a new kwargs dict
218
+    def _argDefault(cl, args, argname, argval):
219
+        if argname not in args:
220
+            args[argname] = argval
221
+        return args
222
+
223
+class EmFieldValue(object):
224
+
225
+    ## Instanciates a EmFieldValue
226
+    # @param name str : The column name associated with this value
227
+    # @param fieldtype EmFieldType: The EmFieldType defining the value
228
+    # @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
229
+    # @throw TypeError if more than 2 arguments given
230
+    # @throw TypeError if fieldtype is not an EmFieldType
231
+    # @throw TypeError if fieldtype is an abstract EmFieldType
232
+    def __init__(self, name, fieldtype, *value):
233
+        if not isinstance(fieldtype, EmFieldType):
234
+            raise TypeError("Expected <class EmFieldType> for 'fieldtype' argument, but got : %s instead" % str(type(fieldtype)))
235
+        if fieldtype.is_abstract:
236
+            raise TypeError("The given fieldtype in argument is abstract.")
237
+
238
+        # This copy ensures that the fieldtype will not change during the value lifecycle
239
+        super(EmFieldValue, self).__setattr__('fieldtype', fieldtype.copy())
240
+
241
+        if len(value) > 1:
242
+            raise TypeError("Accept only 2 positionnal parameters. %s given." % str(len(value)+1))
243
+        elif len(value) == 1:
244
+            self.value = value[0]
59 245
         else:
60
-            return models.BooleanField(**self.options)
61
-
62
-##
63
-#
64
-# Varchar field type
65
-class EmField_char(EmFieldType):
246
+            self.value = fieldtype.default
247
+
248
+        # Use this to set value in the constructor
249
+        setv = super(EmFieldValue, self).__setattr__
250
+        # This copy makes column attributes accessible easily
251
+        for attrname in self.fieldtype.__dict__:
252
+            setv(attrname, getattr(self.fieldtype, attrname))
253
+
254
+        # Assign some EmFieldType methods to the value
255
+        setv('from_python', self.fieldtype.to_value)
256
+        setv('from_string', self.fieldtype.to_value)
257
+        #setv('sqlCol', self.fieldtype.sqlCol)
258
+
259
+    ## The only writable attribute of EmFieldValue is the value
260
+    # @param name str: Have to be value
261
+    # @param value mixed: The value to set
262
+    # @throw AtrributeError if another attribute than value is to be set
263
+    # @throw ValueError if self.to_value raises it
264
+    # @see EmFieldType::to_value()
265
+    def __setattr__(self, name, value):
266
+        if name != "value":
267
+            raise AttributeError("EmFieldValue has only the value property settable")
268
+        super(EmFieldValue,self).__setattr__('value', self.fieldtype.to_value(value))
269
+
270
+        ##
271
+    # @warning When getting the fieldtype you actually get a copy of it to prevent any modifications !
272
+    def __getattr__(self, name):
273
+        if name == 'fieldtype':
274
+            return self.fieldtype.copy()
275
+        return super(EmFieldValue, self).__getattribute__(name)
276
+
277
+    ## @brief Return a valid SQL value
278
+    #
279
+    # Can be used to convert any value (giving one positionnal argument) or to return the current value
280
+    # @param *value list: If a positionnal argument is given return it and not the instance value
281
+    # @return A value suitable for sql
282
+    def to_sql(self, *value):
283
+        if len(value) > 1:
284
+            raise TypeError("Excepted 0 or 1 positional argument but got "+str(len(value)))
285
+        elif len(value) == 1:
286
+            return self.fieldtype.to_sql(value[0])
287
+        return self.fieldtype.to_sql(self.value)
288
+
289
+class EmFieldSQLType(object):
290
+    _integer = {'sql': models.IntegerField}
291
+    _bigint = {'sql': models.BigIntegerField}
292
+    _smallint = {'sql': models.SmallIntegerField}
293
+    _boolean = {'sql': models.BooleanField}
294
+    _nullableboolean = {'sql': models.NullBooleanField}
295
+    _float = {'sql': models.FloatField}
296
+    _varchar = {'sql': models.CharField}
297
+    _text = {'sql': models.TextField}
298
+    _time = {'sql': models.TimeField}
299
+    _date = {'sql': models.DateField}
300
+    _datetime = {'sql': models.DateTimeField}
301
+
302
+    _names = {
303
+        'int': _integer,
304
+        'integer': _integer,
305
+        'bigint': _bigint,
306
+        'smallint': _smallint,
307
+        'boolean': _boolean,
308
+        'bool': _boolean,
309
+        'float': _float,
310
+        'char': _varchar,
311
+        'varchar': _varchar,
312
+        'text': _text,
313
+        'time': _time,
314
+        'date': _date,
315
+        'datetime': _datetime,
316
+    }
317
+
318
+    @classmethod
319
+    def sqlType(cls, name, **kwargs):
320
+        if not isinstance(name, str):
321
+            raise TypeError("Expect <class str>, <class int>|None but got : %s %s" % (str(type(name)), str(type(size))))
322
+        name = name.lower()
323
+        if name not in cls._names:
324
+            raise ValueError("Unknown type '%s'" % name)
325
+
326
+        if name in ['boolean','bool'] and kwargs['nullable'] in [1,'true']:
327
+            sqlclass = _nullableboolean
328
+        else:
329
+            sqlclass = cls._names[name]
66 330
 
67
-    def __init__(self, **kwargs):
68
-        super(EmField_char, self).__init__(**kwargs)
331
+        if len(kwargs) == 0:
332
+            return sqlclass['sql']
69 333
 
70
-    def to_django(self):
71
-        return models.CharField(**self.options)
334
+        return sqlclass['sql'](**kwargs)
72 335
 
73 336
 
74
-##
75
-#
76
-# Date field type
77
-class EmField_date(EmFieldType):
337
+## @brief Represents values with common arithmetic operations
338
+class EmFieldValue_int(EmFieldValue):
339
+    def __int__(self):
340
+        return self.value
78 341
 
79
-    def __init__(self, **kwargs):
80
-        super(EmField_date, self).__init__(**kwargs)
342
+    def __add__(self, other):
343
+        return self.value + other
81 344
 
82
-    def to_django(self):
83
-        return models.DateField(**self.options)
345
+    def __sub__(self, other):
346
+        return self.value - other
84 347
 
348
+    def __mul__(self, other):
349
+        return self.value * other
85 350
 
86
-##
87
-#
88
-# Text field type
89
-class EmField_text(EmFieldType):
90
-    def __init__(self, **kwargs):
91
-        super(EmField_text, self).__init__(**kwargs)
351
+    def __div__(self, other):
352
+        return self.value / other
92 353
 
93
-    def to_django(self):
94
-        return models.TextField(**self.options)
354
+    def __mod__(self, other):
355
+        return self.value % other
95 356
 
357
+    def __iadd__(self, other):
358
+        self.value = int(self.value + other)
359
+        return self
96 360
 
97
-##
98
-#
99
-# Image field type
100
-class EmField_image(EmFieldType):
361
+    def __isub__(self, other):
362
+        self.value = int(self.value - other)
363
+        return self
101 364
 
102
-    def __init__(self, **kwargs):
103
-        super(EmField_image, self).__init__(**kwargs)
365
+    def __imul__(self, other):
366
+        self.value = int(self.value * other)
367
+        return self
104 368
 
105
-    def to_django(self):
106
-        return models.ImageField(**self.options)
369
+    def __idiv__(self, other):
370
+        self.value = int(self.value / other)
371
+        return self
107 372
 
108 373
 
109
-##
110
-#
111
-# File field type
112
-class EmField_file(EmFieldType):
374
+## @brief Handles integer fields
375
+# @note Enforcing type to be int
376
+# @note Default name is 'integer' and default 'valueobject' is EmFieldValue_int
377
+class EmField_integer(EmFieldType):
113 378
 
114 379
     def __init__(self, **kwargs):
115
-        super(EmField_file, self).__init__(**kwargs)
380
+        self._init(kwargs)
381
+        # Default name
382
+        kwargs = self.__class__._argDefault(kwargs, 'name', 'integer')
383
+        # Default value object
384
+        kwargs = self.__class__._argDefault(kwargs, 'valueobject', EmFieldValue_int)
385
+        # Type enforcing
386
+        kwargs = self.__class__._setType(kwargs, 'int')
387
+        super(EmField_integer, self).__init__(**kwargs)
116 388
 
117
-    def to_django(self):
118
-        return models.FileField(**self.options)
389
+    ##
390
+    # @todo catch cast error ?
391
+    def to_sql(self, value):
392
+        return value
119 393
 
394
+    def to_value(self, value):
395
+        if value == None:
396
+            return super(EmField_integer, self).to_value(value)
397
+        return int(value)
120 398
 
121
-##
122
-#
123
-# URL field type
124
-class EmField_url(EmFieldType):
125 399
 
400
+## @brief Handles boolean fields
401
+# @note Enforce type to be 'boolean'
402
+# @note Default name is 'boolean'
403
+class EmField_boolean(EmFieldType):
126 404
     def __init__(self, **kwargs):
127
-        super(EmField_url, self).__init__(**kwargs)
405
+        self._init(kwargs)
406
+        #Default name
407
+        kwargs = self.__class__._argDefault(kwargs, 'name', 'boolean')
408
+        #Type enforcing
409
+        kwargs = self.__class__._setType(kwargs, 'boolean')
410
+        super(EmField_boolean, self).__init__(**kwargs)
411
+        pass
128 412
 
129
-    def to_django(self):
130
-        return models.URLField(**self.options)
413
+    def to_sql(self, value):
414
+        return 1 if super(EmField_boolean, self).to_sql(value) else 0
131 415
 
416
+    def to_value(self, value):
417
+        if value == None:
418
+            return super(EmField_boolean, self).to_value(value)
419
+        self.value = bool(value)
420
+        return self.value
132 421
 
133
-##
134
-#
135
-# Email field type
136
-class EmField_email(EmFieldType):
137 422
 
423
+## @brief Handles string fields
424
+# @note Enforce type to be (varchar)
425
+# @note Default 'name' is 'char'
426
+# @note Default 'type_length' is 76
427
+class EmField_char(EmFieldType):
428
+    default_length = 76
138 429
     def __init__(self, **kwargs):
139
-        super(EmField_email, self).__init__(**kwargs)
140
-
141
-    def to_django(self):
142
-        return models.EmailField(**self.options)
430
+        self._init(kwargs)
431
+        kwargs = self.__class__._argDefault(kwargs, 'type_length', self.__class__.default_length)
432
+        kwargs = self.__class__._argDefault(kwargs, 'name', 'char')
433
+        #Type enforcing
434
+        kwargs = self.__class__._setType(kwargs, 'varchar')
435
+        super(EmField_char, self).__init__(**kwargs)
436
+    def to_sql(self, value):
437
+        return str(value)
143 438
 
144 439
 
145
-##
146
-#
147
-# Datetime field type
148
-class EmField_datetime(EmFieldType):
149
-
440
+## @brief Handles date fields
441
+# @note Enforce type to be 'datetime'
442
+# @todo rename to EmField_datetime
443
+# @todo timezones support
444
+class EmField_date(EmFieldType):
150 445
     def __init__(self, **kwargs):
151
-        super(EmField_datetime, self).__init__(**kwargs)
152
-
153
-    def to_django(self):
154
-        return models.DateTimeField(**self.options)
155
-
446
+        self._init(kwargs)
447
+        kwargs = self.__class__._argDefault(kwargs, 'name', 'date')
448
+        #Type enforcing
449
+        kwargs = self.__class__._setType(kwargs, 'datetime')
450
+        super(EmField_date, self).__init__(**kwargs)
156 451
 
157
-##
158
-#
159
-# Time field type
160
-class EmField_time(EmFieldType):
452
+    def to_sql(self, value):
453
+        return value #thanks to sqlalchemy
161 454
 
162
-    def __init__(self, **kwargs):
163
-        super(EmField_datetime, self).__init__(**kwargs)
455
+    def to_value(self, value):
456
+        if value == None:
457
+            return super(EmField_date, self).to_value(value)
458
+        if isinstance(value, int):
459
+            #assume its a timestamp
460
+            return datetime.fromtimestamp(value)
461
+        if isinstance(value, datetime.datetime):
462
+            return value
164 463
 
165
-    def to_django(self):
166
-        return models.TimeField(**self.options)
167 464
 
168
-
169
-##
170
-#
171
-# mlstring field type
465
+## @brief Handles strings with translations
172 466
 class EmField_mlstring(EmField_char):
173 467
 
174 468
     def __init__(self, **kwargs):
469
+        self._init(kwargs)
470
+        kwargs = self.__class__._argDefault(kwargs, 'name', 'mlstr')
175 471
         super(EmField_mlstring, self).__init__(**kwargs)
176 472
 
177
-    def to_django(self):
178
-        return models.CharField(**self.options)
473
+    def to_sql(self, value):
474
+        return value.__str__()
179 475
 
476
+    def to_value(self, value):
477
+        if value == None:
478
+            return super(EmField_mlstring, self).to_value(value)
479
+        if isinstance(value, str):
480
+            return MlString.load(value)
481
+        elif isinstance(value, MlString):
482
+            return value
483
+        raise TypeError("<class str> or <class MlString> excepted. But got "+str(type(value)))
180 484
 
181
-##
182
-#
183
-# Icon field type
184
-class EmField_icon(EmField_char):
185 485
 
486
+## @brief Handles lodel uid fields
487
+class EmField_uid(EmField_integer):
186 488
     def __init__(self, **kwargs):
187
-        super(EmField_icon, self).__init__(**kwargs)
489
+        self._init(kwargs)
490
+        kwargs = self.__class__._argEnforce(kwargs, 'primarykey', True)
491
+        super(EmField_uid, self).__init__(**kwargs)
492
+        pass
188 493
 
189
-    def to_django(self):
190
-        return models.CharField(**self.options)

Loading…
Cancel
Save