WebアプリフレームワークDjangoの書籍1を勉強中です。 書籍の1章で説明されているコードSnippetの共有アプリをベースに機能を追加して勉強を継続しています。
今回、キーワードとタグでSnippetを検索する機能を追加したので、その内容をメモしています。 機能追加にあたっては、Naritoさんのブログ2を参考にさせて頂きました。
なお、tagのラジオボタンを非選択の状態に戻せない、一般的なtagでの検索のようにボタンを押すだけでの遷移ができない、といった問題があります。 これらの機能を実装したら、追記したいと思います。
検索機能の動作画面
Snippetの一覧ページに検索機能を追加しました。 Snippetのテーブルの上に、検索キーワード、tagのラジオボタン、検索実行ボタンを追加しています。
Pythonタグのラジオボタンをクリックし、検索ボタンを押すとPythonタグを付与されたSnippetが検索されます。
さらに、キーワードに"title"と入力すると、Pythonタグとのand検索が行えます。
ファイル構成
migrationディレクトリなど、機能追加に関係のないディレクトリやファイルは省略しています。
$ tree . ├── __init__.py ├── admin.py ├── apps.py ├── forms.py # ファイル更新 ├── models.py ├── static │ └── snippets │ └── css │ └── style.css ├── templates │ └── snippets │ ├── snippet_delete.html │ ├── snippet_detail.html │ ├── snippet_edit.html │ ├── snippet_new.html │ ├── top.html # ファイル更新 ├── tests.py ├── urls.py └── views.py # ファイル更新
ソースコード
本稿では、ListViewクラスベースで作成したSnippet一覧ページを基にしています。 検索用のSnippetSearchFormクラスを追加し、 ListViewクラスのget_querysetメソッドをオーバーライドすることで検索機能を実装しています。
forms.py
キーワード検索用フォームのkey_word変数と、 tag検索用フォームのtag変数を用意します。 key_wordは入力しないこともあるので、requiredはFalseにしています。
tagはDBに登録されているものを取得して自動でラジオボタンの項目を作成します。 入力項目がいたずらに増えるのを避けるため、 Snippetに一つも登録されていないtagはannotateで表示させないようにしています。 また、querysetはイテラブル3なので、スライスを使って表示件数の上限を設けることもできます。
from taggit.models import Tag from django.db.models import Count class SnippetSearchForm(forms.Form): key_word = forms.CharField( label="検索キーワード", required=False, widget=forms.TextInput(attrs={"class": "form", "autocomplete": "off", "placeholder": "キーワード", }) ) # 登録件数が多い順に、件数1以上のタグのみ表示 tag_queryset = Tag.objects.all().annotate( count=Count("taggit_taggeditem_items")).filter(count__gt=0).order_by("-count") tag = forms.ModelChoiceField( label="タグでの絞り込み", required=False, queryset=tag_queryset, widget=forms.RadioSelect(attrs={"class": "form"}) )
views.py
form.pyで定義したSnippetSearchFormを利用して、ListView内で取得するquerysetを絞り込むようにします。 新たに、get_querysetメソッドを追加しています。 メンバー変数self.formにSnippetSearchFormを設定します。 formの入力が妥当であった場合、key_wordを一つ一つに分解し、or検索を行います。 ここでは、Snippetの説明文、作成者、タイトルでfilter処理をすることで、querysetの絞り込みを行っています。
次に、get_context_dataでformに名前を設定して呼び出せるようにします。 get_context_dataメソッドは元々peginationのために記述していたもので、 context["search_form"] = self.formのみ追加すればよいです。 get_querysetが必ず先に呼ばれる[^2]ため、このような書き方ができるようです。
from snippets.forms import SnippetSearchForm # 追加 from django.db.models import Q # 追加 class TopView(ListView): template_name = "snippets/top.html" paginate_by = 5 model = Snippet context_object_name = "snippets" ordering = "id" # 追加 def get_queryset(self): queryset = super().get_queryset() self.form = form = SnippetSearchForm(self.request.GET or None) if form.is_valid(): # キーワードの絞り込み key_word = form.cleaned_data.get("key_word") if key_word: for word in key_word.split(): queryset = queryset.filter( Q(description__icontains=word) | Q(created_by__username__icontains=word) | Q(title__icontains=word)).distinct() tag = form.cleaned_data.get("tag") if tag: queryset = queryset.filter(tags=tag) return queryset def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(**kwargs) # 追加 context["search_form"] = self.form page: Page = context['page_obj'] context['paginator_range'] = page.paginator.get_elided_page_range( page.number, on_each_side=2, on_ends=1) return context
top.html
最後に、template HTMLです。 ここでは、Snippetの表示用のtableタグの上部に、検索フォームを追加しています。 tagフォームに関しては、for文で回さなくとも1行で書くことはできるのですが、縦に並べられてしまうのを避けるためにfor文で記載しています。
<!-- 追加 --> <form id="search-form" action="" method="GET"> <div class="form-group"> {{ search_form.key_word }} </div> <div class="form-check"> {% for tag in search_form.tag %} {{ tag }} {% endfor %} </div> <button class="btn btn-primary" type="submit">検索</button> </form> <p class="text-primary"> {{ page_obj.paginator.count }}件の検索結果 </p> {% if snippets %} <table class="table"> ... </table>
書籍
まず実際にWebアプリを作り、その後詳細な機能を解説する形式で、Djangoを学ぶことができます。
- 芝田 将 "実践Django Pythonによる本格Webアプリケーション開発"↩
- Django、ブログ②の記事一覧を作る↩
- QuerySet API リファレンス↩