From c80d0b8b3571b1055951ee8e387ec6ee56d5c55b Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Wed, 23 May 2018 17:13:35 +0200 Subject: Wizards - JSON fields: Manage field display in forms - management in wizards save and form init (refs #4089) --- ishtar_common/forms.py | 78 +++++++++++++++++++--- .../migrations/0053_auto_20180523_1126.py | 43 ------------ .../migrations/0053_auto_20180523_1504.py | 45 +++++++++++++ ishtar_common/models.py | 3 + ishtar_common/wizards.py | 45 ++++++++++++- 5 files changed, 158 insertions(+), 56 deletions(-) delete mode 100644 ishtar_common/migrations/0053_auto_20180523_1126.py create mode 100644 ishtar_common/migrations/0053_auto_20180523_1504.py (limited to 'ishtar_common') diff --git a/ishtar_common/forms.py b/ishtar_common/forms.py index c314e4f13..5de0db91d 100644 --- a/ishtar_common/forms.py +++ b/ishtar_common/forms.py @@ -20,6 +20,7 @@ """ Forms definition """ +from collections import OrderedDict import datetime import re import types @@ -107,6 +108,16 @@ def get_readonly_clean(key): return func +JSON_VALUE_TYPES_FIELDS = { + 'T': (forms.CharField, None), + 'LT': (forms.CharField, forms.Textarea), + 'I': (forms.IntegerField, None), + 'F': (forms.FloatField, None), + 'D': (DateField, None), + 'C': (forms.CharField, None), +} + + class CustomForm(object): form_admin_name = "" form_slug = "" @@ -120,8 +131,7 @@ class CustomForm(object): except AttributeError: pass super(CustomForm, self).__init__(*args, **kwargs) - available, excluded = self.check_availability_and_excluded_fields( - current_user) + available, excluded, json_fields = self.check_custom_form(current_user) for exc in excluded: if hasattr(self, 'fields'): self.remove_field(exc) @@ -131,6 +141,25 @@ class CustomForm(object): if exc in form.fields: form.fields.pop(exc) + new_fields = {} + for order, key, field in json_fields: + while order in new_fields: # json fields with the same number + order += 1 + new_fields[order] = (key, field) + + if not new_fields: + return + + # re-order for json fields + fields = OrderedDict() + for idx, field in enumerate(self.fields.items()): + key, c_field = field + if idx + 1 in new_fields: + alt_key, alt_field = new_fields[idx + 1] + fields[alt_key] = alt_field + fields[key] = c_field + self.fields = fields + def are_available(self, keys): for k in keys: if k not in self.fields: @@ -142,9 +171,28 @@ class CustomForm(object): self.fields.pop(key) @classmethod - def check_availability_and_excluded_fields(cls, current_user): + def _get_json_fields(cls, custom_form): + fields = [] + for field in custom_form.json_fields.order_by('order').all(): + key = "data__" + field.json_field.key + field_cls, widget = forms.CharField, None + if field.json_field.value_type in JSON_VALUE_TYPES_FIELDS: + field_cls, widget = JSON_VALUE_TYPES_FIELDS[ + field.json_field.value_type] + attrs = {'label': field.label or field.json_field.name, + 'required': False} + if field.help_text: + attrs['help_text'] = field.help_text + if widget: + attrs['widget'] = widget(attrs={"class": "form-control"}) + f = field_cls(**attrs) + fields.append((field.order or 1, key, f)) + return fields + + @classmethod + def check_custom_form(cls, current_user): if not current_user: - return True, [] + return True, [], [] base_q = {"form": cls.form_slug, 'available': True} # order is important : try for user, user type then all query_dicts = [] @@ -159,23 +207,31 @@ class CustomForm(object): dct = base_q.copy() dct.update({'apply_to_all': True}) query_dicts.append(dct) - excluded_lst = [] + form = None for query_dict in query_dicts: q = models.CustomForm.objects.filter(**query_dict) if not q.count(): continue # todo: prevent multiple result in database form = q.all()[0] - if not form.enabled: - return False, [] - for excluded in form.excluded_fields.all(): - # could have be filtered previously - excluded_lst.append(excluded.field) break - return True, excluded_lst + if not form: + return True, [], [] + if not form.enabled: + return False, [], [] + excluded_lst = [] + for excluded in form.excluded_fields.all(): + # could have be filtered previously + excluded_lst.append(excluded.field) + json_fields = cls._get_json_fields(form) + return True, excluded_lst, json_fields @classmethod def get_custom_fields(cls): + """ + Get fields than can be customized: excluded, re-ordered (WIP) or + re-labeled (WIP) + """ if hasattr(cls, 'base_fields'): fields = cls.base_fields else: diff --git a/ishtar_common/migrations/0053_auto_20180523_1126.py b/ishtar_common/migrations/0053_auto_20180523_1126.py deleted file mode 100644 index f776dce30..000000000 --- a/ishtar_common/migrations/0053_auto_20180523_1126.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.10 on 2018-05-23 11:26 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('ishtar_common', '0052_m2m_images_migrate_imports'), - ] - - operations = [ - migrations.CreateModel( - name='CustomFormJsonField', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('order', models.IntegerField(default=1, verbose_name='Order')), - ('custom_form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='json_fields', to='ishtar_common.CustomForm')), - ], - options={ - 'verbose_name': 'Custom form - Json data field', - 'verbose_name_plural': 'Custom form - Json data fields', - }, - ), - migrations.AddField( - model_name='jsondatafield', - name='value_type', - field=models.CharField(choices=[(b'T', 'Text'), (b'LT', 'Long text'), (b'I', 'Integer'), (b'F', 'Float'), (b'D', 'Date'), (b'C', 'Choices')], default=b'T', max_length=10, verbose_name='Type'), - ), - migrations.AddField( - model_name='customformjsonfield', - name='json_field', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_form_details', to='ishtar_common.JsonDataField'), - ), - migrations.AddField( - model_name='jsondatafield', - name='custom_forms', - field=models.ManyToManyField(blank=True, through='ishtar_common.CustomFormJsonField', to='ishtar_common.CustomForm'), - ), - ] diff --git a/ishtar_common/migrations/0053_auto_20180523_1504.py b/ishtar_common/migrations/0053_auto_20180523_1504.py new file mode 100644 index 000000000..14e34a866 --- /dev/null +++ b/ishtar_common/migrations/0053_auto_20180523_1504.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-05-23 15:04 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0052_m2m_images_migrate_imports'), + ] + + operations = [ + migrations.CreateModel( + name='CustomFormJsonField', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.CharField(blank=True, default=b'', max_length=200, verbose_name='Label')), + ('order', models.IntegerField(default=1, verbose_name='Order')), + ('help_text', models.TextField(blank=True, null=True, verbose_name='Help')), + ('custom_form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='json_fields', to='ishtar_common.CustomForm')), + ], + options={ + 'verbose_name': 'Custom form - Json data field', + 'verbose_name_plural': 'Custom form - Json data fields', + }, + ), + migrations.AddField( + model_name='jsondatafield', + name='value_type', + field=models.CharField(choices=[(b'T', 'Text'), (b'LT', 'Long text'), (b'I', 'Integer'), (b'F', 'Float'), (b'D', 'Date'), (b'C', 'Choices')], default=b'T', max_length=10, verbose_name='Type'), + ), + migrations.AddField( + model_name='customformjsonfield', + name='json_field', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_form_details', to='ishtar_common.JsonDataField'), + ), + migrations.AddField( + model_name='jsondatafield', + name='custom_forms', + field=models.ManyToManyField(blank=True, through='ishtar_common.CustomFormJsonField', to='ishtar_common.CustomForm'), + ), + ] diff --git a/ishtar_common/models.py b/ishtar_common/models.py index bb285883f..e4c6d0e9a 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -1893,7 +1893,10 @@ class CustomFormJsonField(models.Model): custom_form = models.ForeignKey(CustomForm, related_name='json_fields') json_field = models.ForeignKey(JsonDataField, related_name='custom_form_details') + label = models.CharField(_(u"Label"), max_length=200, blank=True, + default='') order = models.IntegerField(verbose_name=_(u"Order"), default=1) + help_text = models.TextField(_(u"Help"), blank=True, null=True) class Meta: verbose_name = _(u"Custom form - Json data field") diff --git a/ishtar_common/wizards.py b/ishtar_common/wizards.py index a6844b674..40044f100 100644 --- a/ishtar_common/wizards.py +++ b/ishtar_common/wizards.py @@ -120,8 +120,12 @@ def filter_no_fields_form(form, other_check=None): if not hasattr(self.request.user, 'ishtaruser'): return False if issubclass(form, CustomForm): - enabled, exc = form.check_availability_and_excluded_fields( + enabled, excluded, json_fields = form.check_custom_form( self.request.user.ishtaruser) + if not hasattr(self, 'json_fields'): + self.json_fields = {} + self.json_fields[form.form_slug] = [ + key for order, key, field in json_fields] if not enabled: return False if other_check: @@ -595,11 +599,27 @@ class Wizard(NamedUrlWizardView): return_object): dct = self.get_extra_model(dct, form_list) obj = self.get_current_saved_object() - # manage dependant items + data = {} + if obj: + data = obj.data + + # manage dependant items and json fields other_objs = {} for k in dct.keys(): if '__' not in k: continue + # manage json field + if k.startswith('data__'): + data_keys = k[len('data__'):].split('__') + # tree + current_data = data + for data_key in data_keys[:-1]: + if data_key not in current_data: + current_data[data_key] = {} + current_data = current_data[data_key] + current_data[data_keys[-1]] = dct.pop(k) + continue + vals = k.split('__') assert len(vals) == 2, \ "Only one level of dependant item is managed" @@ -627,6 +647,7 @@ class Wizard(NamedUrlWizardView): elif type(dct[k]) not in (list, tuple): dct[k] = [dct[k]] setattr(obj, k, dct[k]) + obj.data = data if hasattr(obj, 'pre_save'): obj.pre_save() try: @@ -711,6 +732,7 @@ class Wizard(NamedUrlWizardView): except ValidationError as e: logger.warning(unicode(e)) return self.render(form_list[-1]) + obj.data = data obj.save(**saved_args) for k in adds: getattr(obj, k).add(adds[k]) @@ -1243,6 +1265,25 @@ class Wizard(NamedUrlWizardView): Get initial data from an object: simple form """ initial = MultiValueDict() + + # manage json field + if hasattr(self, 'json_fields') \ + and getattr(c_form, 'form_slug', None) \ + and c_form.form_slug in self.json_fields \ + and obj.data: + for key in self.json_fields[c_form.form_slug]: + if not key.startswith('data__'): + continue + json_keys = key[len('data__'):].split('__') + value = obj.data + for json_key in json_keys: + if json_key not in value: + value = None + break + value = value[json_key] + if value: + initial[key] = value + for base_field in c_form.base_fields.keys(): value = obj base_model = None -- cgit v1.2.3