|
@@ -1,370 +0,0 @@
|
1
|
|
-# -*- coding: utf-8 -*-
|
2
|
|
-
|
3
|
|
-import os
|
4
|
|
-import sys
|
5
|
|
-
|
6
|
|
-import django
|
7
|
|
-from django.db import models
|
8
|
|
-from django.db.models.loading import cache as django_cache
|
9
|
|
-from django.core.exceptions import ValidationError
|
10
|
|
-from django.contrib import admin
|
11
|
|
-from EditorialModel.migrationhandler.dummy import DummyMigrationHandler
|
12
|
|
-from EditorialModel.classtypes import EmNature
|
13
|
|
-
|
14
|
|
-from EditorialModel.exceptions import *
|
15
|
|
-
|
16
|
|
-## @package EditorialModel::migrationhandler::django
|
17
|
|
-# @deprecated Broken since EmField and fieldtypes changed
|
18
|
|
-
|
19
|
|
-## @brief Create a django model
|
20
|
|
-# @param name str : The django model name
|
21
|
|
-# @param fields dict : A dict that contains fields name and type ( str => DjangoField )
|
22
|
|
-# @param app_label str : The name of the applications that will have those models
|
23
|
|
-# @param module str : The module name this model will belong to
|
24
|
|
-# @param options dict : Dict of options (name => value)
|
25
|
|
-# @param admin_opts dict : Dict of options for admin part of this model
|
26
|
|
-# @param parent_class str : Parent class name
|
27
|
|
-# @return A dynamically created django model
|
28
|
|
-# @note Source : https://code.djangoproject.com/wiki/DynamicModels
|
29
|
|
-#
|
30
|
|
-def create_model(name, fields=None, app_label='', module='', options=None, admin_opts=None, parent_class=None):
|
31
|
|
- class Meta:
|
32
|
|
- # Using type('Meta', ...) gives a dictproxy error during model creation
|
33
|
|
- pass
|
34
|
|
-
|
35
|
|
- if app_label:
|
36
|
|
- # app_label must be set using the Meta inner class
|
37
|
|
- setattr(Meta, 'app_label', app_label)
|
38
|
|
-
|
39
|
|
- # Update Meta with any options that were provided
|
40
|
|
- if options is not None:
|
41
|
|
- for key, value in options.iteritems():
|
42
|
|
- setattr(Meta, key, value)
|
43
|
|
-
|
44
|
|
- # Set up a dictionary to simulate declarations within a class
|
45
|
|
- attrs = {'__module__': module, 'Meta': Meta}
|
46
|
|
-
|
47
|
|
- # Add in any fields that were provided
|
48
|
|
- if fields:
|
49
|
|
- attrs.update(fields)
|
50
|
|
-
|
51
|
|
- # Create the class, which automatically triggers ModelBase processing
|
52
|
|
- if parent_class is None:
|
53
|
|
- parent_class = models.Model
|
54
|
|
- model = type(name, (parent_class,), attrs)
|
55
|
|
-
|
56
|
|
- # Create an Admin class if admin options were provided
|
57
|
|
- if admin_opts is not None:
|
58
|
|
- class Admin(admin.ModelAdmin):
|
59
|
|
- pass
|
60
|
|
- for key, value in admin_opts:
|
61
|
|
- setattr(Admin, key, value)
|
62
|
|
- admin.site.register(model, Admin)
|
63
|
|
- return model
|
64
|
|
-
|
65
|
|
-
|
66
|
|
-## @package EditorialModel.migrationhandler.django
|
67
|
|
-# @brief A migration handler for django ORM
|
68
|
|
-#
|
69
|
|
-# Create django models according to the editorial model
|
70
|
|
-
|
71
|
|
-class DjangoMigrationHandler(DummyMigrationHandler):
|
72
|
|
-
|
73
|
|
- ## @brief Instanciate a new DjangoMigrationHandler
|
74
|
|
- # @param app_name str : The django application name for models generation
|
75
|
|
- # @param debug bool : Set to True to be in debug mode
|
76
|
|
- # @param dryrun bool : If true don't do any migration, only simulate them
|
77
|
|
- def __init__(self, app_name, debug=False, dryrun=False):
|
78
|
|
- self.debug = debug
|
79
|
|
- self.app_name = app_name
|
80
|
|
- self.dryrun = dryrun
|
81
|
|
-
|
82
|
|
- ## @brief Record a change in the EditorialModel and indicate wether or not it is possible to make it
|
83
|
|
- # @note The states ( initial_state and new_state ) contains only fields that changes
|
84
|
|
- #
|
85
|
|
- # @note Migration is not applied by this method. This method only checks if the new em is valid
|
86
|
|
- #
|
87
|
|
- # @param em model : The EditorialModel.model object to provide the global context
|
88
|
|
- # @param uid int : The uid of the change EmComponent
|
89
|
|
- # @param initial_state dict | None : dict with field name as key and field value as value. Representing the original state. None mean creation of a new component.
|
90
|
|
- # @param new_state dict | None : dict with field name as key and field value as value. Representing the new state. None mean component deletion
|
91
|
|
- # @throw EditorialModel.exceptions.MigrationHandlerChangeError if the change was refused
|
92
|
|
- # @todo Some tests about strating django in this method
|
93
|
|
- # @todo Rename in something like "validate_change"
|
94
|
|
- #
|
95
|
|
- # @warning broken because of : https://code.djangoproject.com/ticket/24735 you have to patch django/core/management/commands/makemigrations.py w/django/core/management/commands/makemigrations.py
|
96
|
|
- def register_change(self, em, uid, initial_state, new_state):
|
97
|
|
-
|
98
|
|
- #Starting django
|
99
|
|
- os.environ['LODEL_MIGRATION_HANDLER_TESTS'] = 'YES'
|
100
|
|
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Lodel.settings")
|
101
|
|
- django.setup()
|
102
|
|
- from django.contrib import admin
|
103
|
|
- from django.core.management import call_command as django_cmd
|
104
|
|
-
|
105
|
|
- if self.debug:
|
106
|
|
- self.dump_migration(uid, initial_state, new_state)
|
107
|
|
-
|
108
|
|
- #Generation django models
|
109
|
|
- self.em_to_models(em)
|
110
|
|
- try:
|
111
|
|
- #Calling makemigrations to see if the migration is valid
|
112
|
|
- #django_cmd('makemigrations', self.app_name, dry_run=True, interactive=False, merge=True)
|
113
|
|
- django_cmd('makemigrations', self.app_name, dry_run=True, interactive=False)
|
114
|
|
- except django.core.management.base.CommandError as e:
|
115
|
|
- raise MigrationHandlerChangeError(str(e))
|
116
|
|
-
|
117
|
|
- return True
|
118
|
|
-
|
119
|
|
- ## @brief Print a debug message representing a migration
|
120
|
|
- # @param uid int : The EmComponent uid
|
121
|
|
- # @param initial_state dict | None : dict representing the fields that are changing
|
122
|
|
- # @param new_state dict | None : dict represnting the new fields states
|
123
|
|
- def dump_migration(self, uid, initial_state, new_state):
|
124
|
|
- if self.debug:
|
125
|
|
- print("\n##############")
|
126
|
|
- print("DummyMigrationHandler debug. Changes for component with uid %d :" % uid)
|
127
|
|
- if initial_state is None:
|
128
|
|
- print("Component creation (uid = %d): \n\t" % uid, new_state)
|
129
|
|
- elif new_state is None:
|
130
|
|
- print("Component deletion (uid = %d): \n\t" % uid, initial_state)
|
131
|
|
- else:
|
132
|
|
- field_list = set(initial_state.keys()).union(set(new_state.keys()))
|
133
|
|
- for field_name in field_list:
|
134
|
|
- str_chg = "\t%s " % field_name
|
135
|
|
- if field_name in initial_state:
|
136
|
|
- str_chg += "'" + str(initial_state[field_name]) + "'"
|
137
|
|
- else:
|
138
|
|
- str_chg += " creating "
|
139
|
|
- str_chg += " => "
|
140
|
|
- if field_name in new_state:
|
141
|
|
- str_chg += "'" + str(new_state[field_name]) + "'"
|
142
|
|
- else:
|
143
|
|
- str_chg += " deletion "
|
144
|
|
- print(str_chg)
|
145
|
|
- print("##############\n")
|
146
|
|
- pass
|
147
|
|
-
|
148
|
|
- ## @brief Register a new model state and update the data representation given the new state
|
149
|
|
- # @param em model : The EditorialModel to migrate
|
150
|
|
- # @param state_hash str : Note usefull (for the moment ?)
|
151
|
|
- # @todo Rename this method in something like "model_migrate"
|
152
|
|
- def register_model_state(self, em, state_hash):
|
153
|
|
- if self.dryrun:
|
154
|
|
- return
|
155
|
|
- if self.debug:
|
156
|
|
- print("Applying editorial model change")
|
157
|
|
-
|
158
|
|
- #Starting django
|
159
|
|
- os.environ['LODEL_MIGRATION_HANDLER_TESTS'] = 'YES'
|
160
|
|
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Lodel.settings")
|
161
|
|
- django.setup()
|
162
|
|
- from django.contrib import admin
|
163
|
|
- from django.core.management import call_command as django_cmd
|
164
|
|
-
|
165
|
|
- #Generation django models
|
166
|
|
- self.em_to_models(em)
|
167
|
|
- try:
|
168
|
|
- #Calling makemigrations
|
169
|
|
- django_cmd('makemigrations', self.app_name, interactive=False)
|
170
|
|
- except django.core.management.base.CommandError as e:
|
171
|
|
- raise MigrationHandlerChangeError(str(e))
|
172
|
|
-
|
173
|
|
- try:
|
174
|
|
- #Calling migrate to update the database schema
|
175
|
|
- django_cmd('migrate', self.app_name, interactive=False, noinput=True)
|
176
|
|
- except django.core.management.base.CommandError as e:
|
177
|
|
- raise MigrationHandlerChangeError("Unable to migrate to new model state : %s" % e)
|
178
|
|
-
|
179
|
|
- pass
|
180
|
|
-
|
181
|
|
- ## @brief Return the models save method
|
182
|
|
- #
|
183
|
|
- # The save function of our class and type models is used to set unconditionnaly the
|
184
|
|
- # classtype, class_name and type_name models property
|
185
|
|
- #
|
186
|
|
- # @param classname str: The classname used to call the super().save
|
187
|
|
- # @param level str: Wich type of model are we doing. Possible values are 'class' and 'type'
|
188
|
|
- # @param datas list : List of property => value to set in the save function
|
189
|
|
- # @return The wanted save function
|
190
|
|
- @staticmethod
|
191
|
|
- def set_save_fun(class_obj, level, datas):
|
192
|
|
-
|
193
|
|
- if level == 'class':
|
194
|
|
- def save(self, *args, **kwargs):
|
195
|
|
- #Sets the classtype and class name
|
196
|
|
- self.classtype = datas['classtype']
|
197
|
|
- self.class_name = datas['class_name']
|
198
|
|
- super(class_obj, self).save(*args, **kwargs)
|
199
|
|
- elif level == 'type':
|
200
|
|
- def save(self, *args, **kwargs):
|
201
|
|
- #Set the type name
|
202
|
|
- self.type_name = datas['type_name']
|
203
|
|
- super(class_obj, self).save(*args, **kwargs)
|
204
|
|
-
|
205
|
|
- class_obj.save = save
|
206
|
|
-
|
207
|
|
- return class_obj
|
208
|
|
-
|
209
|
|
- ## @brief Create django models from an EditorialModel.model object
|
210
|
|
- # @param edMod EditorialModel.model.Model : The editorial model instance
|
211
|
|
- # @return a dict with all the models
|
212
|
|
- # @todo Handle fieldgroups
|
213
|
|
- # @todo write and use a function to forge models name from EmClasses and EmTypes names
|
214
|
|
- # @note There is a problem with the related_name for superiors fk : The related name cannot be subordinates, it has to be the subordinates em_type name
|
215
|
|
- def em_to_models(self, edMod):
|
216
|
|
-
|
217
|
|
- module_name = self.app_name + '.models'
|
218
|
|
-
|
219
|
|
- #Purging django models cache
|
220
|
|
- if self.app_name in django_cache.all_models:
|
221
|
|
- for modname in django_cache.all_models[self.app_name]:
|
222
|
|
- del(django_cache.all_models[self.app_name][modname])
|
223
|
|
- #del(django_cache.all_models[self.app_name])
|
224
|
|
-
|
225
|
|
- app_name = self.app_name
|
226
|
|
- #Creating the document model
|
227
|
|
- document_attrs = {
|
228
|
|
- 'lodel_id': models.AutoField(primary_key=True),
|
229
|
|
- 'classtype': models.CharField(max_length=16, editable=False),
|
230
|
|
- 'class_name': models.CharField(max_length=76, editable=False),
|
231
|
|
- 'type_name': models.CharField(max_length=76, editable=False),
|
232
|
|
- 'string': models.CharField(max_length=255),
|
233
|
|
- 'date_update': models.DateTimeField(auto_now=True, auto_now_add=True),
|
234
|
|
- 'date_create': models.DateTimeField(auto_now_add=True),
|
235
|
|
- 'rank': models.IntegerField(),
|
236
|
|
- 'help_text': models.CharField(max_length=255),
|
237
|
|
- }
|
238
|
|
-
|
239
|
|
- #Creating the base model document
|
240
|
|
- document_model = create_model('document', document_attrs, self.app_name, module_name)
|
241
|
|
-
|
242
|
|
- django_models = {'doc': document_model, 'classes': {}, 'types': {}}
|
243
|
|
-
|
244
|
|
- classes = edMod.classes()
|
245
|
|
-
|
246
|
|
- def object_repr(self):
|
247
|
|
- return self.string
|
248
|
|
-
|
249
|
|
- #Creating the EmClasses models with document inheritance
|
250
|
|
- for emclass in classes:
|
251
|
|
- emclass_fields = {
|
252
|
|
- 'save': None, #will be set later using self.set_save_fun
|
253
|
|
- '__str__': object_repr,
|
254
|
|
- }
|
255
|
|
-
|
256
|
|
- #Addding non optionnal fields
|
257
|
|
- for emfield in emclass.fields():
|
258
|
|
- if not emfield.optional:
|
259
|
|
- # !!! Replace with fieldtype 2 django converter
|
260
|
|
- #emclass_fields[emfield.uniq_name] = models.CharField(max_length=56, default=emfield.uniq_name)
|
261
|
|
- emclass_fields[emfield.uniq_name] = self.field_to_django(emfield, emclass)
|
262
|
|
- #print("Model for class %s created with fields : "%emclass.uniq_name, emclass_fields)
|
263
|
|
- if self.debug:
|
264
|
|
- print("Model for class %s created" % emclass.uniq_name)
|
265
|
|
- django_models['classes'][emclass.uniq_name] = create_model(emclass.uniq_name, emclass_fields, self.app_name, module_name, parent_class=django_models['doc'])
|
266
|
|
-
|
267
|
|
- self.set_save_fun(django_models['classes'][emclass.uniq_name], 'class', {'classtype': emclass.classtype, 'class_name': emclass.uniq_name})
|
268
|
|
-
|
269
|
|
-
|
270
|
|
- #Creating the EmTypes models with EmClass inherithance
|
271
|
|
- for emtype in emclass.types():
|
272
|
|
- emtype_fields = {
|
273
|
|
- 'save':None,
|
274
|
|
- '__str__': object_repr,
|
275
|
|
- }
|
276
|
|
- #Adding selected optionnal fields
|
277
|
|
- for emfield in emtype.selected_fields():
|
278
|
|
- #emtype_fields[emfield.uniq_name] = models.CharField(max_length=56, default=emfield.uniq_name)
|
279
|
|
- emtype_fields[emfield.uniq_name] = self.field_to_django(emfield, emtype)
|
280
|
|
- #Adding superiors foreign key
|
281
|
|
- for nature, superiors_list in emtype.superiors().items():
|
282
|
|
- for superior in superiors_list:
|
283
|
|
- emtype_fields[nature+'_'+superior.uniq_name] = models.ForeignKey(superior.uniq_name, related_name=emtype.uniq_name, blank=True, default=None, null=True)
|
284
|
|
-
|
285
|
|
- # Method to get the parent that is not None
|
286
|
|
- emtype_fields['sup_field_list'] = [ nature + '_' + superior.uniq_name for superior in superiors_list ]
|
287
|
|
- def get_sup(self):
|
288
|
|
- for field in [ getattr(self, fname) for fname in self.sup_field_list ]:
|
289
|
|
- if not (field is None):
|
290
|
|
- return field
|
291
|
|
- return None
|
292
|
|
- #Adding a method to get the superior by nature
|
293
|
|
- emtype_fields[nature] = get_sup
|
294
|
|
-
|
295
|
|
- # Adding a dummy function to non present nature
|
296
|
|
- def dummyNone(self): return None
|
297
|
|
- for nat in [ nat for nat in EmNature.getall() if nat not in emtype_fields]:
|
298
|
|
- emtype_fields[nat] = dummyNone
|
299
|
|
-
|
300
|
|
-
|
301
|
|
- if self.debug:
|
302
|
|
- print("Model for type %s created" % emtype.uniq_name)
|
303
|
|
- django_models['types'][emtype.uniq_name] = create_model(emtype.uniq_name, emtype_fields, self.app_name, module_name, parent_class=django_models['classes'][emclass.uniq_name], admin_opts=dict())
|
304
|
|
- self.set_save_fun(django_models['types'][emtype.uniq_name], 'type', {'type_name': emtype.uniq_name})
|
305
|
|
- return django_models
|
306
|
|
-
|
307
|
|
- ## @brief Return a good django field type given a field
|
308
|
|
- # @param f EmField : an EmField object
|
309
|
|
- # @param assoc_comp EmComponent : The associated component (type or class)
|
310
|
|
- # @return A django field instance
|
311
|
|
- # @note The manytomany fields created with the rel2type field has no related_name associated to it
|
312
|
|
- def field_to_django(self, f, assoc_comp):
|
313
|
|
-
|
314
|
|
- #Building the args dictionnary for django field instanciation
|
315
|
|
- args = dict()
|
316
|
|
- args['null'] = f.nullable
|
317
|
|
- if not (f.default is None):
|
318
|
|
- args['default'] = f.default
|
319
|
|
- v_fun = f.validation_function(raise_e=ValidationError)
|
320
|
|
- if v_fun:
|
321
|
|
- args['validators'] = [v_fun]
|
322
|
|
- if f.uniq:
|
323
|
|
- args['unique'] = True
|
324
|
|
-
|
325
|
|
- # Field instanciation
|
326
|
|
- if f.ftype == 'char': # varchar field
|
327
|
|
- args['max_length'] = f.max_length
|
328
|
|
- return models.CharField(**args)
|
329
|
|
- elif f.ftype == 'int': # integer field
|
330
|
|
- return models.IntegerField(**args)
|
331
|
|
- elif f.ftype == 'text': # text field
|
332
|
|
- return models.TextField(**args)
|
333
|
|
- elif f.ftype == 'datetime': # Datetime field
|
334
|
|
- args['auto_now'] = f.now_on_update
|
335
|
|
- args['auto_now_add'] = f.now_on_create
|
336
|
|
- return models.DateTimeField(**args)
|
337
|
|
- elif f.ftype == 'bool': # Boolean field
|
338
|
|
- if args['null']:
|
339
|
|
- return models.NullBooleanField(**args)
|
340
|
|
- del(args['null'])
|
341
|
|
- return models.BooleanField(**args)
|
342
|
|
- elif f.ftype == 'rel2type': # Relation to type
|
343
|
|
-
|
344
|
|
- if assoc_comp is None:
|
345
|
|
- raise RuntimeError("Rel2type field in a rel2type table is not allowed")
|
346
|
|
- #create first a throught model if there is data field associated with the relation
|
347
|
|
- kwargs = dict()
|
348
|
|
-
|
349
|
|
- relf_l = f.get_related_fields()
|
350
|
|
- if len(relf_l) > 0:
|
351
|
|
- through_fields = {}
|
352
|
|
-
|
353
|
|
- #The two FK of the through model
|
354
|
|
- through_fields[assoc_comp.name] = models.ForeignKey(assoc_comp.uniq_name)
|
355
|
|
- rtype = f.get_related_type()
|
356
|
|
- through_fields[rtype.name] = models.ForeignKey(rtype.uniq_name)
|
357
|
|
-
|
358
|
|
- for relf in relf_l:
|
359
|
|
- through_fields[relf.name] = self.field_to_django(relf, None)
|
360
|
|
-
|
361
|
|
- #through_model_name = f.uniq_name+assoc_comp.uniq_name+'to'+rtype.uniq_name
|
362
|
|
- through_model_name = f.name + assoc_comp.name + 'to' + rtype.name
|
363
|
|
- module_name = self.app_name + '.models'
|
364
|
|
- #model created
|
365
|
|
- through_model = create_model(through_model_name, through_fields, self.app_name, module_name)
|
366
|
|
- kwargs['through'] = through_model_name
|
367
|
|
-
|
368
|
|
- return models.ManyToManyField(f.get_related_type().uniq_name, **kwargs)
|
369
|
|
- else: # Unknow data type
|
370
|
|
- raise NotImplemented("The conversion to django fields is not yet implemented for %s field type" % f.ftype)
|