Djangoフォームフィールド-ネストされたテーブル

こんにちは、habrauser。



私は、「ネストされたテーブル」タイプのdjangoフォームフィールドをXML形式のデータストレージで実装した記事を提案します。

これは、フィールドとdjangoウィジェットの動作をよりよく理解し、任意のフィールドを作成するための一歩を踏み出すことに興味がある人に役立ちます。

すでにこれを知っているなら、その記事はあなたにとって面白くないかもしれません。











djangoの1つのワークフロー

構造化された要素の配列のドキュメントフィールド(テーブル)への入力をサポートする必要があります。



オプション間の1週間の審議の後

-インラインフォームセット

-添付文書(このような機能は既に存在します)

-XML / JSONのシリアル化を使用したカスタムフィールド/ウィジェット

XMLのフォームセットが選択されました



インラインフォームセットは、アーキテクチャが非常に複雑であるため拒否されました。

-作成後にのみインラインで保存する必要があります(ドキュメントを保存する方法に入ります)

-別のモデルが必要、

-モデルフォーム



添付文書も適合しませんでした(そのようなフィールドごとに独自の文書構造を作成しないでください)



カスタムフィールドのアイデアはさらに引き付けられました。

すべてのロジックをフィールド/ウィジェットに入れて、それを忘れることができます。

このアプローチにより、システムアーキテクチャの複雑さが最小限になります。



JSONの便利な作業(ロード、ダンプ)にもかかわらず、

SQLを使用してデータベースからレポートを生成する必要があるため、XMLが選択されました。

PostgreSQLがJSONの使用をサポートしている場合、Oracleではバージョン12でのみ表示されます。

XMLを操作する場合、xpathを介してデータベースレベルのインデックスを使用できます。



SQLレベルの作業
--  XML   select t.id, (xpath('/item/@n_phone', nt))[1] as n_phone1, (xpath('/item/@is_primary', nt))[1] as is_primary1, (xpath('/item/@n_phone', nt))[2] as n_phone2, (xpath('/item/@is_primary', nt))[2] as is_primary2 from docflow_document17 t cross join unnest(xpath('/xml/item', t.nested_table::xml)) as nt; --   XML- select t.id from docflow_document17 t where t.id = 2 and ('1231234', 'False') in ( select (xpath('/item/@n_phone', nt_row))[1]::text, (xpath('/item/@is_primary', nt_row))[1]::text from unnest(xpath('/xml/item', t.nested_table::xml)) as nt_row );
      
      









最初は、すぐに機能するウィジェットが作成されました。

-renderメソッドで受け入れられたXML

-生成および表示されたフォームセット

-value_from_datadictでformsetが生成され、データパラメーターを取得、検証、XMLを収集して出力

すべて完璧に機能し、非常にシンプルでした。
 class XMLTableWidget(widgets_django.Textarea): class Media: js = (settings.STATIC_URL + 'forms_custom/xmltable.js',) def __init__(self, formset_class, attrs=None): super(XMLTableWidget, self).__init__(attrs=None) self._Formset = formset_class def render(self, name, value, attrs=None): initial = [] if value: xml = etree.fromstring(value) for row in xml: initial.append(row.attrib) formset = self._Formset(initial=initial, prefix=name) return render_to_string('forms_custom/xmltable.html', {'formset': formset}) def value_from_datadict(self, data, files, name): u"""    ,    XML  -  formset-  ,    initial- :    formset-  ,       """ formset_data = {k: v for k, v in data.items() if k.startswith(name)} formset = self._Formset(data=formset_data, prefix=name) if formset.is_valid(): from lxml.builder import E xml_items = [] for item in formset.cleaned_data: if item and not item[formset_deletion_field_name]: del item[formset_deletion_field_name] item = {k: unicode(v) for k, v in item.items()} xml_items.append(E.item("", item)) xml = E.xml(*xml_items) return etree.tostring(xml, pretty_print=False) else: initial_value = data.get('initial-%s' % name) if initial_value: return initial_value else: raise Exception(_('Error in table and initial not find'))
      
      









1つの警告がない場合:通常のフォーム検証の不可能性。

もちろん、フォームセットを可能な限りソフトにし、XMLをキャッチして、フィールドレベルまたはフォームレベルでデータを検証できます。

おそらくウィジェットにis_formset_valid属性を保存し、self.widget.is_formset_valid型のフィールドからチェックすることができます。

しかし、どういうわけか悪くなりました。



フィールドとウィジェットの共同作業を行う必要があります。

結果は次のとおりです。



私は、ソースコードを読み直すことを気にしないことに決めました。

代わりに、メソッドについて過度にコメントしました。

主なアイデアは、さまざまな入力パラメーターを標準化することです。

-フィールドの初期化中に受信したXML

-ウィジェットの出力に関するデータを含む辞書

-適切に準備された設計

{"formset":formset、 "xml_initial":xml_string}のような単一の形式に変換します

そして、「技術の問題」



XMLTableFieldフィールド
 class XMLTableField(fields.Field): widget = widgets_custom.XMLTableWidget hidden_widget = widgets_custom.XMLTableHiddenWidget default_error_messages = {'invalid': _('Error in table')} def __init__(self, formset_class, form_prefix, *args, **kwargs): kwargs['show_hidden_initial'] = True #       super(XMLTableField, self).__init__(*args, **kwargs) self._formset_class = formset_class self._formset_prefix = form_prefix self._procss_widget_data_cache = {} self._prepare_value_cache = {} def prepare_value(self, value): u"""                    unicode,   XML,        initial  ,     POST-,   ,   ,     formset,  xml_initial   hidden_initial .      show_hidden_initial = True     ,    . """ if value is None: return {'xml_initial': value, 'formset': self._formset_class(initial=[], prefix=self._formset_prefix)} elif type(value) == unicode: value_hash = hash(value) if value_hash not in self._prepare_value_cache: initial = [] if value: xml = etree.fromstring(value) for row in xml: #    'False'  False, #    XML    , #     bool attrs = {} for k,v in row.attrib.items(): attrs[k] = False if v == 'False' else v initial.append(attrs) formset = self._formset_class(initial=initial, prefix=self._formset_prefix) self._prepare_value_cache[value_hash] = formset return {'xml_initial': value, 'formset': self._prepare_value_cache[value_hash]} elif type(value) == dict: if 'xml' not in value: formset = self._widget_data_to_formset(value) return {'xml_initial': value['initial'], 'formset': formset} return value def clean(self, value): u"""       ,  ,    formset-,    formset   XML   _formset_to_xml   ValidationError,  formset   """ formset = self._widget_data_to_formset(value, 'clean') return self._formset_to_xml(formset) def _formset_to_xml(self, formset): u"""   XML    .   _has_changed    XML   clean    cleaned_data """ if formset.is_valid(): from lxml.builder import E xml_items = [] for item in formset.cleaned_data: if item and not item.get(formset_deletion_field_name, False): if formset_deletion_field_name in item: del item[formset_deletion_field_name] item = {k: unicode(v) for k, v in item.items()} xml_items.append(E.item("", item)) xml = E.xml(*xml_items) xml_str = etree.tostring(xml, pretty_print=False) return xml_str else: raise ValidationError(self.error_messages['invalid'], code='invalid') def _widget_data_to_formset(self, value, call_from=None): u"""   POST-,   formset-   ,    prepare_value     ,     FormSet-      """ #     -   self.prepare_value formset_hash = hash(frozenset(value.items())) if formset_hash not in self._procss_widget_data_cache: formset = self._formset_class(data=value, prefix=self._formset_prefix) self._procss_widget_data_cache[formset_hash] = formset return formset else: return self._procss_widget_data_cache[formset_hash] def _has_changed(self, initial, data): u"""     .     formset   ,   XML   c  ,   initial-   XML """ formset = self._widget_data_to_formset(data) try: data_value = self._formset_to_xml(formset) except ValidationError: return True return data_value != initial
      
      









XMLTableHiddenWidget
 class XMLTableHiddenWidget(widgets_django.HiddenInput): def render(self, name, value, attrs=None): u"""    xml_initial    render """ value = value['xml_initial'] return super(XMLTableHiddenWidget, self).render(name, value, attrs)
      
      









XMLTableWidget
 class XMLTableWidget(widgets_django.Widget): class Media: js = (settings.STATIC_URL + 'forms_custom/xmltable.js',) def render(self, name, value, attrs=None): u"""    formset,   initial   data   ,     """ formset = value['formset'] return render_to_string('forms_custom/xmltable.html', {'formset': formset}) def value_from_datadict(self, data, files, name): u"""    ,   formset-     clean     ,  initial-,        """ formset_data = {k: v for k, v in data.items() if k.startswith(name)} initial_key = 'initial-%s' % name formset_data['initial'] = data[initial_key] return formset_data
      
      









この場合、主なタスクは最大のコンパクトさを確保することでした

XMLTableWidget-テンプレート
 {% load base_tags %} {% load base_filters %} {{formset.management_form}} {% if formset.non_field_errors %} <div class='alert alert-danger'> {% for error in form.non_field_errors %} {{ error }}<br/> {% endfor %} </div> {% endif %} <table> {% for form in formset %} {% if forloop.first %} <tr> {% for field in form.visible_fields %} {% if field.name == 'DELETE' %} <td></td> {% else %} <td>{{field.label}}</td> {% endif %} {% endfor %} </tr> {% endif %} <tr> {% for field in form.visible_fields %} {% if field.name == 'DELETE' %} <th > <div class='hide'>{{field}}</div> <a onclick="xmltable_mark_deleted(this, '{{field.auto_id}}')" class="pointer"> <span class="glyphicon glyphicon-remove"></span> </a> </th> {% else %} <td> {{ field|add_widget_css:"form-control" }} {% if field.errors %} <span class="help-block"> {% for error in field.errors %} {{ error }}<br/> {% endfor %} </span> {% endif %} </td> {% endif %} {% endfor %} </tr> {% endfor %} </table>
      
      









標準のチェックボックスを「X」アイコンに置き換えます

削除用にマークするときに行に色を付けます

XMLTableWidget-スクリプト
 function xmltable_mark_deleted(p_a, p_checkbox_id) { var chb = $('#' + p_checkbox_id) var row = $(p_a).parents('tr') if(chb.prop('checked')) { chb.removeProp('checked') row.css('background-color', 'white') } else { chb.attr('checked', '1') row.css('background-color', '#f2dede') } }
      
      









基本的には以上です。

このフィールドを使用して複雑なテーブルを取得し、必要に応じて検証することができます

それほど複雑ではないシステムコード



ユーザーはFormSetを準備するだけです。

XMLTableWidget
 class NestedTableForm(forms.Form): phone_type = forms.ChoiceField(label=u"", choices=[('', '---'), ('1', '.'), ('2', '.')], required=False) n_phone = forms.CharField(label=u"", required=False) is_primary = forms.BooleanField(label=u"", required=False, widget=forms.CheckboxInput(check_test=boolean_check)) nested_table_formset_class = formset_factory(NestedTableForm, can_delete=True)
      
      









このフィールドを取得します。



djangoのアプリケーションでリポジトリへのリンクを提供します。このフィールドには、このフィールドがあります。

アプリケーションを接続するか、フィールド/ウィジェット/テンプレート/スクリプトのコードをどこにでもコピーできます。

bitbucket.org/dibrovsd/django_forms_custom/src



All Articles