電話入力フィールドを例として使用したDjangoフォームライブラリの問題

ご存じのとおり、Djangoにはhtmlフォームを生成および管理するためのライブラリが含まれています。 昔々、別のフォームライブラリがDjangoに付属していましたが、その後完全に書き直されました。 おそらく、その後、開発者は多くのアーキテクチャ上の問題を解決しました。 しかし、現在のライブラリを使用する場合、いくつかの困難があります。 それが私が話したいことです。



だから挑戦。 ユーザーは、携帯電話やその他の個人情報をサイトに残すのが大好きです。 さらに、正しく入力する方法を考えずにこれを行いたい:8(908)1271669または、例えば908 127 16 69。 16-25、+ 7 968 127-31-32。 正規化された形式で、つまり登録なしで数値を検証および保存する必要があることがわかりました。 これから説明するフィールドには、複数の電話番号を入力できます。 ストレージ形式は、スペースで区切られた11桁のシーケンスとして定義されます。



さらにナレーションを行うには、フォームの動作の原理を簡単に説明する必要があります。 フォームは、Formクラスと、フォームに含まれるフィールドのセット(Fieldクラス)で構成されます。 フォームが最初に作成されると、初期辞書、つまりフィールドの初期値が転送されます。 ModelFormに関しては、フォームの作成時に渡されたモデルインスタンスから初期辞書が自動的に作成されます。 Formクラスは、htmlフォーム自体のコードを生成するためのインターフェースを提供します。 このプロセスはBoundFieldクラスのインスタンスによって実行され、フォームに含まれるフィールドとデータをリンクします。 htmlコード自体は、ウィジェット(Widgetクラス)によって生成されます。 ユーザーが完成したフォームを送信すると、データコンストラクターがフォームコンストラクター(POST要求のコンテンツ)に転送されます。 これで、フォームフィールドはユーザー入力をチェックし、すべてのフィールドが正しく入力されていることを確認する必要があります。 エラーの場合、フォームは再び生成されますが、初期辞書は生成されませんが、ユーザー入力辞書データはフィールドの値として取得されます。



ご覧のとおり、フォーム内のデータには3つのルートがあります:アプリケーションからユーザー(フォームが最初に作成されたときの初期)、ユーザーからユーザー(誤って入力されたデータの再表示)、およびユーザーからアプリケーション(入力されたデータが正しい場合) まあ、タスクは簡単に思えます。 1番目と3番目のルートに侵入して、ユーザー用に電話をフォーマットし、アプリケーション用に正規化する必要があります。 最後から始めましょう。



最初に、将来のフィールドの空白を作成します。 明らかに、CharFieldから継承する必要があります。



class MultiplePhoneFormField(forms.CharField): #    ,      . phone_code = ''
      
      





ドキュメントには、検証中の値の処理に関係するすべてのメソッドが記載されています。 to_python()は、アプリケーションの正しいデータ型にキャストするために使用されます。 ただし、データ型は文字列であるため、このメソッドは使用しません。 次に、validate()およびrun_validators()メソッド。 入力された値の正確さを検証するために使用されますが、変更することはできないため、適切ではありません。 残っているのは、フィールドのclean()メソッドです。 基本実装では、上記のメソッドを正しい順序で呼び出し、最終値を返します。 そこで、ここにコードを配置します。



  def clean(self, phones): phones = super(MultiplePhoneFormField, self).clean(phones) cleaned_phones = [] for phone in phones.split(','): phone = re.sub(r'[\s +.()\-]', '', phone) if not phone: continue if not phone.isdigit(): raise ValidationError(u'   .') if len(phone) == 11: pass elif len(phone) == 10: phone = '7' + phone elif len(self.phone_code + phone) == 11: phone = self.phone_code + phone else: raise ValidationError(u'  .') cleaned_phones.append(phone) return ' '.join(cleaned_phones)
      
      





数値の検証方法については詳しく説明しませんが、すべてが明確であると思います。



次に、アプリケーションからユーザーへのルート。 ドキュメントには、アプリケーションに電子メールアドレスのリストを返すMultiEmailFieldフィールドの実装例があります。 ただし、このリストをユーザーに表示する方法については説明していません。 どうやら、このタスクはフォームを作成するアプリケーションの肩の上にあることが暗示されています。 他の例もありません。 しかし、私たちは誇りに思っていません、ソースで見ることができます。



BoundFieldクラスにはas_widget()メソッドがあります。このメソッドは、value()メソッドを呼び出すことにより、表示するフィールドの値を実際のウィジェットに渡します。 このメソッドでは、データのソース(データまたは初期)が決定されます。 そして、ここで大きな失望を待っています。データが初期から取得された場合、フィールドをプロセスに組み込み、データを変更することはできません。 value()メソッドは単純にself.form.initial.get(self.name)を呼び出し、データソースに関係なく、それらをフィールドのprepare_value()メソッドに渡します。 すべての値が同じパイプラインを通過し、最後に「正しい」値を取得する必要があることがわかります。



何かを理解していないか、Djangovフォームが実際に設計されているので、アプリケーションだけがフォームに出力するためのデータを準備できます。 フォーム作成時の初期辞書には、すでにhtmlに挿入できるデータが用意されているはずです。



「しかし、待ってください。DatetimeFieldはどのように機能しますか。これは、datetimeを初期値として冷静に受け入れますか?」 だから私はどのように考えました。 未知のソースから取得した値は、DateTimeInputウィジェットのrender()メソッドに渡され、そのメソッドが_format_value()メソッドに渡します。 そして、すでにこのメソッドは、値が日時であることがわかると、それを文字列に変換します。 私たちの場合、なぜこれもできないのですか? アプリケーションから渡され、フォームが送信されたときに受け取った値のタイプが同じだからです。 どちらの場合も、これは文字列です。



それにもかかわらず、解決策が必要です。 BoundField.value()メソッドをもう一度見ると、ユーザーから受け取った値がさらにbound_data()メソッドに渡されることがわかります。 そのため、値が後に続くprepare_value()メソッドでは、最初にマークを付けると、値がどこから来たかを判別できます。 やってみましょう。



 class ValueFromDatadict(unicode): pass class MultiplePhoneFormField(forms.CharField): #    ,      . phone_code = '' def bound_data(self, data, initial): return ValueFromDatadict(data) def prepare_value(self, value): if not value or isinstance(value, ValueFromDatadict): return value return ', '.join(format_phones(value, code=self.phone_code))
      
      





やった! これで、電話は、フォームに初めて表示されるときにフォーマットされ、編集されたデータがユーザーから送られてきても変更されません。 そして、あなたは電話をフォーマットすることができます。



 def format_phones(phones, code=None): for phone in filter(None, phones.split(' ')): if len(phone) != 11: #  . pass elif phone[0:4] == '8800': # 8 800 100-31-32 phone = u'8 800 %s-%s-%s' % (phone[4:7], phone[7:9], phone[9:11]) elif code and phone.startswith(code): # (+7 351) 722-16-25 # (+7 3512) 22-16-25 phone = phone[len(code):] phone = u'(+%s %s) %s-%s-%s' % (code[0], code[1:], phone[:-4], phone[-4:-2], phone[-2:]) else: # +7 968 127-31-32 phone = u'+%s %s %s-%s-%s' % (phone[0], phone[1:4], phone[4:7], phone[7:9], phone[9:11]) yield phone
      
      





編集されたオブジェクトが接続されている都市コードをフォームコンストラクターで示すためにのみ残ります。



 class RestaurantForm(forms.ModelForm): phone = MultiplePhoneFormField(label=u'', required=False, help_text=u'     .') def __init__(self, *args, **kwargs): super(RestaurantForm, self).__init__(*args, **kwargs) if self.instance: self.fields['phone'].phone_code = self.instance.city.phone_code
      
      





さて、サイトに電話を表示するには、このフィルターが適しています。



 @register.filter def format_phones_with_code(phones, code): return mark_safe(u', '.join([u'<nobr>%s</nobr>' % phone for phone in format_phones(phones, code)]))
      
      





もちろん、タスクが解決されたと想定できます。 しかし、松葉杖がないわけではありません。 Djangoにバンドルされているフィールド実装についても同じことが言えます。 たとえば、同じDateTimeFieldフィールドのto_python()メソッドでは、値が既にdatetime型であるかどうかのチェックがあります。 この場合、to_python()メソッドは、データディクショナリから取得した値に対してのみ呼び出されます。 データディクショナリの内容に関するフォームドキュメントには 、「これらは通常文字列ですが、文字列である必要はありません」と明記されています。 どうやら、これは投稿要求からのユーザー入力以外の何かを検証するために何らかの意味があります。 しかし、このような柔軟性は不確実性をもたらし、検証をアルゴリズム的なタスクというよりもヒューリスティックにします。



All Articles