Djangoの自動複数選択フィールド

こんにちは、Habr。

前回の記事では、Djangoにタグを入力するためのカスタムフィールドを作成する技術について説明しました。 ここで、AJAXによる自動補完機能を備えた多肢選択フィールドを実装する既製の多かれ少なかれ普遍的なソリューションを共有したいと思います。 このフィールドと前の記事で説明したフィールドとの違いは、ディレクトリからアイテムを選択できるだけで、新しいアイテムは作成できないことです。 素晴らしいjQueryプラグインSelect2がフロントエンド部分を担当します。 ソリューションは、別個のDjangoアプリケーションとして発行されます。





ウィジェット



from django.forms.widgets import Widget from django.conf import settings from django.utils.safestring import mark_safe from django.template.loader import render_to_string class InfiniteChoiceWidget(Widget): '''Infinite choice widget, based on Select2 jQuery-plugin (http://ivaynberg.github.io/select2/)''' class Media: js = ( settings.STATIC_URL + "select2/select2.js", ) css = { 'all': (settings.STATIC_URL + 'select2/select2.css',) } def __init__(self, data_model, multiple=False, disabled=False, attrs=None): super(InfiniteChoiceWidget, self).__init__(attrs) self.data_model = data_model self.multiple = multiple self.disabled = disabled def render(self, name, value, attrs=None): return mark_safe(render_to_string("infinite_choice_widget.html", {"disabled": self.disabled, "multiple": self.multiple, "attrs": attrs, "app_name": self.data_model._meta.app_label, "model_name": self.data_model.__name__, "input_name": name, "current_value": value if value else "", }) )
      
      





ウィジェットコンストラクタは、複数の選択とフィールドアクティビティをそれぞれ担当するモデルクラス、複数フラグ、および無効フラグを受け入れます。 MediaサブクラスはSelect2スクリプトとスタイルを接続します。 Select2プラグインを初期化するスクリプトは、infinite_choice_widget.htmlテンプレートに記述されます。

 {% load url from future %} {#    django 1.5 #} <input type="hidden" {% for key,value in attrs.iteritems %} {{key}}="{{value}}" {% endfor %} name="{{input_name}}" value="{{current_value}}" /> {# Settings for Select2 #} <script type="text/javascript"> $("#{{attrs.id}}").select2({ multiple: {{multiple|yesno:"true,false"}}, formatInputTooShort: function (input, min) { return ",  " + (min - input.length) + "   "; }, formatSearching: function () { return "..."; }, formatNoMatches: function () { return "  "; }, minimumInputLength: 3, initSelection : function (element, callback) { $.ajax({ url: "{% url 'infinite_choice_data' app_name model_name %}", type: 'GET', dataType: 'json', data: {ids: element.val()}, success: function(data, textStatus, xhr) { callback(data); }, error: function(xhr, textStatus, errorThrown) { callback({}); } }); }, ajax: { url: "{% url 'infinite_choice_data' app_name model_name %}", dataType: 'json', data: function (term, page) { return {term: term, // search term page_limit: 10 }; }, results: function (data, page) { return {results: data}; } } }); {% if disabled %} $("#{{attrs.id}}").select2("disable"); {% endif %} </script>
      
      





ここでの中心的なオブジェクトは、選択されたオプションの識別子を保存する隠された入力です。 そしてすでに#idによってSelect2プラグインが設定されています。

プラグインのパラメーターを調べてみましょう。



アプリケーションの名前とこのアプリケーションのモデル名は、データが要求されるURLに示されていることに注意してください。 これは、処理表現を普遍化するために行われます。ビューは、データの取得元のモデルを明確に知る必要はなく、毎回通知します。

最後に、ウィジェットがdisabled = Trueを渡した場合、プラグインが非アクティブであることを伝えます。



オートコンプリート作業の提出



先ほど言ったように、ビューは普遍的でなければならず、どのモデルからデータを取得するかをまったく知る必要はありません。 よく知られているアプリケーション名とモデルクラス名からモデルクラスを取得する標準的な方法があります。これらの行をビューに渡して使用します。

 from django.http import Http404, HttpResponse import json from django.db.models import get_model def infinite_choice_data(request, app_name, model_name): '''Returns data for infinite choice field''' data = [] if not request.is_ajax(): raise Http404 model = get_model(app_name, model_name) if 'term' in request.GET: term = request.GET['term'] page_limit = request.GET['page_limit'] data = model.objects.filter(title__startswith=term)[:int(page_limit)] json_data = json.dumps([{"id": item.id, "text": unicode(item.title)} for item in data]) if 'ids' in request.GET: id_list = request.GET['ids'].split(',') items = model.objects.filter(pk__in=id_list) json_data = json.dumps([{"id": item.id, "text": item.title} for item in items]) response = HttpResponse(json_data, content_type="application/json") response['Cache-Control'] = 'max-age=86400' return response
      
      





django.db.models.get_model関数はモデルクラスを返します。 さらに、リクエスト変数に応じて、文字列という用語で始まるバリアントまたはids変数で渡されたものと等しいidを持つバリアントがモデルから選択されます。 2番目のケースは、以前に入力されたデータでプラグインが初期化されるときに発生します。

応答にCache-Controlヘッダーを追加しました。キャッシュデータの有効期間は1日です。 これは、同じ種類のクエリのデータベースをプルしないようにするためです。 KLADR / FIASのような巨大なディレクトリを持つフィールドを使用する場合に役立ちます。



そして、これがurls.pyのエントリが私たちのビューを探す方法です。

 from django.conf.urls import patterns, url from views import infinite_choice_data urlpatterns = patterns('', url(r'^(?P<app_name>[\w\d_]+)/(?P<model_name>[\w\d_]+)/$', view=infinite_choice_data, name='infinite_choice_data'), )
      
      







フォームフィールド



ご存じのとおり、djangoのフォームフィールドは、主に入力されたデータを検証するために使用されます。 フィールドクラスは次のようになります。

 from django.forms import fields as f_fields from django.core.exceptions import ValidationError from django.core import validators from django.utils.translation import ugettext_lazy as _ class InfiniteChoiceField(f_fields.Field): '''Infinite choice field''' default_error_messages = { 'invalid_choice': _(u'Select a valid choice. %(value)s is not one of the available choices.'), } def __init__(self, data_model, multiple=False, disabled=False, widget_attrs=None, **kwargs): self.data_model = data_model self.disabled = disabled self.multiple = multiple widget = InfiniteChoiceWidget(data_model, multiple, disabled, widget_attrs) super(InfiniteChoiceField, self).__init__(widget=widget, **kwargs) def to_python(self, value): if value in validators.EMPTY_VALUES: return None if self.multiple: values = value.split(',') qs = self.data_model.objects.filter(pk__in=values) pks = set([i.pk for i in qs]) for val in values: if not int(val) in pks: raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) return qs else: try: return self.data_model.objects.get(pk=value) except self.data_model.DoesNotExists: raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) def prepare_value(self, value): if value is not None and hasattr(value, '__iter__') and self.multiple: return u','.join(unicode(v.pk) for v in value) return unicode(value.pk)
      
      





ModelMultipleChoiceFieldを継承しませんでした。これはquerysetで機能し、モデルを使用する必要があるためです。

コンストラクターは、渡されたモデル、複数のフラグと無効化されたフラグ、および特定の属性でウィジェットを初期化します。

to_pythonメソッドは、カンマで区切られた同じ行で単一のIDまたは複数のIDを値として受け取り、複数フラグに応じて処理します。 どちらの場合も、モデル内の選択したIDの存在がチェックされます。

prepare_valueメソッドは、表示用に初期化データを準備します。モデルの単一のインスタンスがフィールドの初期パラメーターで渡される場合、このインスタンスのIDを文字列として返します。 インスタンスのリストまたはQuerySetが渡された場合、IDがコンマで区切られた行を返します。



おわりに



畑はすぐに食べられます。 アプリケーションはここからダウンロードできます 。 フィールドの使用は非常に簡単です。

 from django import forms from infinite_choice_field import InfiniteChoiceField from models import ChoiceModel class TestForm(forms.Form): choice = InfiniteChoiceField(ChoiceModel, multiple=True, disabled=False, required=False, initial=ChoiceModel.objects.filter(id__in=(7, 8, 12)))
      
      





ChoiceModelは任意のビューモデルです

 class ChoiceModel(models.Model): title = models.CharField(max_length=100, verbose_name="Choice title") class Meta: verbose_name = 'ChoiceModel' verbose_name_plural = 'ChoiceModels' def __unicode__(self): return self.title
      
      





settings.pyでアプリケーションを接続することを忘れないでください。

 INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'infinite_choice_field', ... }
      
      





インポートURL

 urlpatterns = patterns('', # Examples: # url(r'^$', 'habr_test.views.home', name='home'), url(r'^', include('core.urls')), url(r'^infinite_choice_data/', include('infinite_choice_field.urls')), # Uncomment the next line to enable the admin: url(r'^admin/', include(admin.site.urls)), )
      
      





テンプレート内のTestFormフォームの統計を表示します

 <!doctype html> <html> <head> <title>Test InfiniteChoiceField</title> {{test_form.media}} </head> <body> <form action=""> {{test_form}} </form> </body> </html>
      
      






All Articles