[Django]SearchViewクラス

Djangoのクラスベースのビューを使って検索画面を作成する場合、FromViewとListViewを使うのが選択肢となる。しかし、同じページで処理する場合には、二つのクラスの機能を合わせたViewが必要となってくる。調べたところ、いいものが見つからず、自分でクラスを作成した。

https://github.com/Arisophy/django-searchview

レポジトリーには、サンプルコード、データも入っています。

1.SearchViewとSearchFormのコード

追加で使うクラスは、SearchViewとSearchFormクラスです。GitHubを参照して下さい。

[GitHub]

パッケージをインストールするか、これらの2つのファイルをプロジェクトにインポートし、SearchFormクラスとSearchViewクラスを使用することで、検索ページを簡単に実装できる。

pip install django-serachview-lib

2.使い方

基本的に、FormViewListViewの使い方と同じ。SearchViewの特徴として、特別な部分は以下。

1)first_display_all_list

Trueの場合、最初のページのobject_listは、SearchFormの初期値から検索される。 Falseの場合、最初のページのobject_listは空となる。

2)form_class

SearchView.form_classに設定するのは、SearchFormクラスである。

3)検索条件の指定方法

  • SearchView.get_form_conditionsでは、SearchFormでのField変数名を、Field Lookup検索条件のLHSとなる。
  • 変数名を使わない場合や、複雑な検索条件は、SearchView.get_form_conditionsをオーバーライドする。
  • annotate“が必要な場合は、get_annotated_querysetをオーバーライドして追加する。

4)ページネーション

POSTで遷移すること。GETで要求した場合は、404エラーとなる。

5)success_url

FormとListが同じページとなったので、FormView.success_urlは使われない。

3.検索画面の実装例

1)SearchForm,SearchViewを取り込む

pip install django-searchview-lib

あるいは、views.pyとforms.pyを、プロジェクトの適当なライブラリ用のフォルダに配置する。

2)Formのサンプル

SearchFormクラスを使用して、検索フィールドを定義する必要がある。フィールドの名前は検索条件になる。以下はサンプルで、自分のDBモデルに合わせて作成する。

[forms.py]
from django.forms import Form
 :
# Test For SearchView
from searchview.forms import SearchForm

class MusicianSearchForm(SearchForm):
    """Search for Musicians."""

    first_name = forms.CharField(
        label='first_name =',
        required = False,
        max_length=50,
    )
    last_name__istartswith = forms.CharField(
        label="last_name like 'val%'",
        required = False,
        max_length=50,
    )
    instrument__contains = forms.CharField(
        label="instrument like '%val%'",
        required = False,
        max_length=32,
    )
    album_count__gte = forms.IntegerField(
        label='album_count >=',
        required = False,
        initial=1,
        min_value=0,
        max_value=999,
    )
    album_count__lt = forms.IntegerField(
        label='album_count <',
        required = False,
        min_value=0,
        max_value=999,
    )

class AlbumSearchForm(SearchForm):
    """Search for Albums."""

   :
    artist__first_name__contains = forms.CharField(
        label="Musician.first_name like '%val%'",
        required = False,
        max_length=50,
    )

3)Viewのサンプル

SearchViewクラスから、検索画面のビューを定義する。

[views.py]
 :
# Test For SearchView
from django.db.models import Count
from searchview.views import SearchView
from .forms import MusicianSearchForm,AlbumSearchForm
from .models import Musician,Album

class AlbumSearchListView(SearchView):
    template_name = 'app/album.html'
    form_class = AlbumSearchForm
    model = Album
    paginate_by = 5
    first_display_all_list = False
    ordering='-release_date'

class MusicianSearchListView(SearchView):
    template_name = 'app/musician.html'
    form_class = MusicianSearchForm
    model = Musician
    paginate_by = 5
    first_display_all_list = True
    ordering='first_name'

    def get_annotated_queryset(self, queryset, cleaned_data):
        # 'album_count'
        queryset = queryset.annotate(
            album_count=Count('album'))

        return queryset

4)Urlpatternsのサンプル

 :
from sample import views
 :
urlpatterns = [
   :
    # Test
    path('', views.AlbumSearchListView.as_view(), name='album'),
    path('musician/', views.MusicianSearchListView.as_view(), name='musician'),
   :
]
 :

5)テンプレートのサンプル

テンプレートについては、FormViewで作っていたテンプレートと、ListViewで作っていたテンプレートを、混ぜて一つのテンプレートに入れることができる。ただ、1点だけ、ページネーションを使う場合は、POSTで要求するようにすること。

 :
{% block content %}
 :
<div class="row">
   <div class="col-md-6">
        <h2>Search Condition</h2>
        <div class="table-responsive">
          <form method="post" id="search_form">
            {% csrf_token %}
            <table class="table table-striped table-sm">
              <tbody>
              {{ form.as_table }}
              </tbody>
            </table>
            <p class="text-center"><input class="btn btn-primary" type="submit" value="Search"></p>
            <input id="pagenation_page" type="hidden" name="page" />
          </form>
        </div>
    </div>
    <div class="col-md-6">
        <h2>Search Results(Album)</h2>
        <div class="table-responsive">
          <table class="table table-striped table-sm">
            <thead>
              <tr>
                <th>#</th>
                <th>name</th>
                <th>release_date</th>
                <th>num_stars</th>
                <th>artist</th>
              </tr>
            </thead>
            <tbody>
            {% for album in object_list %}
              <tr>
                <td>{{ album.id }}</td>
                <td>{{ album.name }}</td>
                <td>{{ album.release_date }}</td>
                <td>{{ album.num_stars }}</td>
                <td>{{ album.artist.first_name }}</td>
              </tr>
            {% endfor %}
            </tbody>
          </table>
        </div>
        <div class="pagination">
          <span class="step-links">
              {% if page_obj.has_previous %}
                  <button type="submit" class="page-btn" value="1">&laquo; first</button>
                  <button type="submit" class="page-btn" value="{{ page_obj.previous_page_number }}">previous</button>
              {% endif %}

              <span class="current">
                  Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
              </span>

              {% if page_obj.has_next %}
                  <button type="submit" class="page-btn" value="{{ page_obj.next_page_number }}">next</button>
                  <button type="submit" class="page-btn" value="{{ page_obj.paginator.num_pages }}">last &raquo;</button>
              {% endif %}
          </span>
        </div>
    </div>
</div>

{% endblock %}

{% block scripts %}
<script>
  $(function(){
    $('.page-btn').click(function(){
      var form1 = document.forms['search_form'];
      var pageno = $(this).val();
      $('#pagenation_page').val(pageno);
      form1.submit();
      return false;
    });
  })
 </script>
{% endblock %}

【Django】検索機能の実装

https://docs.djangoproject.com/ja/3.1/topics/class-based-views/

1.検索画面の実装

FormViewを使用して実装する。

FormView

1-1)forms.py:検索フォームの定義

以下ドキュメント。

Django の Form クラス

class Form

Formで定義するField(項目)について、以下を参照。

Form fields

目的に応じた、検索用のフォームをforms.pyに定義する。

from django.forms import Form
 :
class UserSearchForm(Form):
    name = forms.CharField(
        initial='',
        label='名前',
        required = False,
        max_length=32,
    )
  :

1-2)view.py:拡張FormViewの定義

FormViewをカスタマイズしたクラスを、以下のような感じで作成する。

from django.forms import Form
 :
class UersSearchView(FormView):
    template_name = 'app/search.html'
    form_class = UserSearchForm
    success_url = '/user_list/'  # 2のListViewで定義
   :

1-3)url.pyの定義

urlpatternsに、定義を追加。

 :
from app.views import UersSearchView
 :
urlpatterns = [
   :
    path('search/', UserSearchView.as_view(
            extra_context=
            {
                'title':'検索',
         :
            },
        ), name='search'),
   :
]
 :

2.結果表示

ListViewを使用して実装する。

ListView

ドキュメントを参考に簡単に実装はできる。

3.同じページに結果を表示する

検索条件と結果を同じページに表示したい場合にどうするか?

色々調べて検討した結果、クラスベースのViewでやる場合、FormとListViewを組み合わせた新たなViewを作る必要がある。

ということで、作りました。

BootstrapのTips

自分のためのメモというかリンク集。ドキュメントでよく見るところのリンクとか。

リンクは、基本的にBootstrapの4.5ドキュメント。

1)Colors

https://getbootstrap.jp/docs/4.5/utilities/colors/

2)Alerts

https://getbootstrap.jp/docs/4.5/components/alerts/

3)Buttons

https://getbootstrap.jp/docs/4.5/components/buttons/

リンクをボタンにする場合は、以下。

https://getbootstrap.jp/docs/4.5/components/buttons/#button-tags

4)アイコン

https://icons.getbootstrap.com/

https://feathericons.com/

5)Tables

https://getbootstrap.jp/docs/4.4/content/tables/

6)Grid system

https://getbootstrap.jp/docs/4.5/layout/grid/

この「わかりやすく説明すると」以下を一読する必要あり。

7)Flex

FlexBoxについては以下ページ。

https://getbootstrap.jp/docs/4.5/utilities/flex/#enable-flex-behaviors

https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox

右寄せ、左寄せの制御は以下が使える。

https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content

8)モーダルダイアログ

https://getbootstrap.jp/docs/4.5/components/modal/

【Django】ログイン機能の実装

既存システムからDjangoへのリプレースで、ログイン機能をどう実装するかを検討して、実装した時のメモ。

0.事前検討

(前提)

  • 既存システムDBには、既にID、パスワードソルト、パスワードハッシュ値がある。
  • 既存ユーザーには、ログイン方法の変更の通知はできる。

(方針)

  • この際、IDをメールアドレスに変更する。
  • なるべく、Djangoのフレームに乗っ取る形で実装する。
  • パスワードも再設定が可能なので、DjangoのUserモデル認証に切り替える。
  • 将来は、メールベースの二段階認証にする。

(参考ページ)

Djangoの認証システムを使用する

1.【ログイン】LoginViewで実装(1)

ログイン部分については、LoginViewを使用して実装する。

LoginView

基にしたVisual Studio のサンプルが、admin機能で利用しているので、利用者向けのログイン機能を追加する。

1-1)forms.py:ログインフォームの定義

forms.pyにユーザー用のログインフォームを定義する。Visual Studio のサンプルで既に定義されていたので、それをコピーして修正した。

変更点としては、usernameをメールアドレスで入力させるようにする。Userモデルにはusernameのほかにもemailも持っているがとりあえず。

class UserAuthenticationForm(AuthenticationForm):
    """Authentication for User Login."""
    username = forms.CharField(max_length=254,
                               widget=forms.EmailInput({
                                  'class': 'form-control',
                                  'placeholder': 'Email アドレス'}))
    password = forms.CharField(label=_("Password"),
                               widget=forms.PasswordInput({
                                   'class': 'form-control',
                                   'placeholder':'パスワード'}))

基底のAutheticationFormについては、以下を参照した。

class AuthenticationForm 

(GitHub)django.contrib.auth.forms.py

※今回は、従来のDBを使ったりとかしてカスタマイズすることも可能だとは思うが、手抜きで行くことに。

1-2)template:ログインページの定義

app/login.htmlというファイルで、テンプレートを作成。内容は、LoginViewを参考に。

1-3)url.pyの定義

urlpatternsに、ログインとログアウトを追加。

 :
from django.contrib.auth.views import LoginView, LogoutView
from app import forms, views
 :
urlpatterns = [
   :
    path('login/',
         LoginView.as_view
         (
             template_name='app/login.html',
             authentication_form=forms.UserAuthenticationForm,
             extra_context=
             {
                 'title': 'Login',
             },
         ),
         name='login'),
    path('logout/', LogoutView.as_view(next_page='/'), name='logout'),
   :
]
 :

1-4)setting.pyの定義

ログイン後の最初のページを「LOGIN_REDIRECT_URL」で定義する。

LOGIN_REDIRECT_URL

2.【ログイン】LoginViewで実装(2)

(1)では、Userのusernameにメールアドレスを入れるという強引な方法にしたが、ユーザー名も使いたい場合は、認証でUser.emailをチェックするように修正する。

Django の認証方法のカスタマイズ

具体的には、認証バックエンドを拡張して、カスタマイズすればいい。

認証バックエンドの実装

(GitHub)django.contrib.auth.backends.py

ベースのModelBackend.authenticateでは、以下のように取得している。

class ModelBackend(BaseBackend):
    :
    def authenticate(self, request, username=None, password=None, **kwargs):
        :
        user = UserModel._default_manager.get_by_natural_key(username)
        :

拡張クラスでは、DBのemailからusernameを検索するようにすればいいはず。とりあえずは、今回の運用は(1)の実装で問題ないので確認はしてないが、たぶん。

3.ログイン画面にお知らせ表示

ログイン画面の下に、DBから取得したお知らせ情報を表示するようにしたいので、修正する。

3-1)views.py:login関数の追加

1.ではLoginViewをurl.pyから直接呼び出すことで、views.pyの修正はなかったが、DBデータを取得するためにviews.pyに関数を追加する。

def login(request):
    """Renders the login page."""
    assert isinstance(request, HttpRequest)

    # news list
    news_list = Information.news.order_by('-from_time') # DBからお知らせ情報を取得

    view_obj = LoginView()
    view_obj.request = request
    view_obj.form_class = UserAuthenticationForm
    view_obj.template_name = 'app/login.html'
    view_obj.extra_context = {
        'title':'Login',
        'news_list':news_list,         # 追加のお知らせ情報リスト
    }

    return view_obj.dispatch(request)

3-2)url.pyの定義

LoginViewを直接呼び出していたのを、login関数に変更。

 :
from django.contrib.auth.views import LogoutView
from app import forms, views
 :
urlpatterns = [
   :
    path('login/', views.login, name='login'),     # 変更部分
    path('logout/', LogoutView.as_view(next_page='/'), name='logout'),
   :
]
 :

4.ログインが必要なページ

ログインが必要なページについては、以下でログインしていない場合、ログイン画面にリダイレクトさせる。

login_required デコレータ

クラスビューを使っている場合は、MixInを使う。

LoginRequired mixin

5.二段階認証の追加

これは、別記事で後日。

DjangoのTips

自分のためのメモ。リンクは、基本的にDjangoの3.1ドキュメント。

(Django公式)

Django3.1 ドキュメント

Django3.1 APIリファレンス

(Djangoソース)

https://github.com/django/django/tree/master/django

(DBに関してはこっち)

1.テンプレート

1.1 include

テンプレートの中から、テンプレートを呼び出せる。

https://docs.djangoproject.com/ja/3.1/ref/templates/builtins/#include

方針としては、templates/app/commonというようなフォルダを作って共通のテンプレート部品を置いて、includeして利用する。

1.2 オーバライドしたblockで、基底の呼び出し

https://docs.djangoproject.com/ja/3.1/howto/overriding-templates/#extending-an-overridden-template

以下を呼び出せばいい。注意点として基底側で、「static」を使っている場合、「load static」を呼ばないとエラーになる。

{{ block.super }}

1.3 改行のBRタグへの変更

「linebreaksbr」というフィルタを使えばいい。

https://docs.djangoproject.com/ja/3.1/ref/templates/builtins/#linebreaksbr

{{ value|linebreaksbr }}

1.4 フィルタ

「|」は、テンプレート言語のフィルタというもの。

https://docs.djangoproject.com/ja/3.1/ref/templates/language/#filters

組み込みフィルタリファレンス

自分でフィルタを登録することもできるらしい。

https://docs.djangoproject.com/ja/3.1/howto/custom-template-tags/#writing-custom-template-filters

Djangoのアドオンのhumanizeも使える。

https://docs.djangoproject.com/ja/3.1/ref/contrib/humanize/

桁区切りを入れる、intcommaだが、自分のアプリではうまくいかずにソースを見たところ、言語設定の関係がうまく動作しなかったよう。とりあぜう、第二引数をFalseを設定して回避できた。

1.5 footerのコピーライトの年号

Visual Studioのサンプルだと、ビューで年を変数にしてテンプレートに渡しているが、テンプレートの「now」を使えばわざわざ渡す必要は無くなる。

https://docs.djangoproject.com/ja/3.1/ref/templates/builtins/#now

コピーライト部分は、以下のような感じで書けばいい。

<p class="text-center">
 &copy; gijutsu.com 2019-{% now "Y" %}
</p>

1.6 メニューのアクティブ制御

共通メニューの、ページ毎での”active”制御を、どう実装するか。以下の回答が参考になる。

https://stackoverflow.com/questions/46617375/how-do-i-show-an-active-link-in-a-django-navigation-bar-dropdown-list?answertab=votes#tab-top

ドキュメントはこちら。

ResolverMatch

urls.pyのurlpatternsで定義したnameでチェックすることで実現。

    {% with request.resolver_match.url_name as url_name %}
      <ul class="navbar-nav mr-auto">
        <li class="nav-item {% if url_name == 'home' %}active{% endif %}">
          <a class="nav-link" href="{% url 'home' %}">Home</a>
        </li>
        <li class="nav-item {% if url_name == 'contact' %}active{% endif %}">
          <a class="nav-link" href="{% url 'contact' %}">お問合せ</a>
        </li>
      </ul>
    {% endwith %}

2.セッション

https://docs.djangoproject.com/ja/3.1/topics/http/sessions/

具体的なViewでの使い方は、以下。

ビューでセッションを使う

3.モデル

https://gijutsu.com/2021/01/12/django-db/

バリデータの設定は以下に

https://docs.djangoproject.com/ja/3.2/ref/validators/

4.ビュー

ビルトインのクラスベースビュー API

1)ログインが必要なページ

ログインが必要なページについては、以下でログインしていない場合、ログイン画面にリダイレクトさせる。

login_required デコレータ

クラスビューを使っている場合は、MixInを使う。

LoginRequired mixin

5.フォーム

https://docs.djangoproject.com/en/3.1/topics/forms/

Form fields

ビルトインの Field クラス

モデルからフォームを作成する

6.役に立つパッケージ

※国内限定なら以下で十分

https://qiita.com/xKxAxKx/items/86bdf0bc4c7dc9ee65d9

7.ログ

https://docs.djangoproject.com/ja/3.1/topics/logging/#examples

8.URL ディスパッチャ

https://docs.djangoproject.com/ja/3.1/topics/http/urls/

コントローラの中で、URLを取り出す場合は、reverseを使う。

https://docs.djangoproject.com/ja/3.1/topics/http/urls/#reverse-resolution-of-urls

from django.urls import reverse

:

return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))

9.メッセージ

https://docs.djangoproject.com/ja/3.1/ref/contrib/messages/

10.リクエストとレスポンスのオブジェクト

https://docs.djangoproject.com/ja/3.1/ref/request-response/

(処理後に呼び出しページに戻る例)

    return HttpResponseRedirect(request.META['HTTP_REFERER'])

同じnameの入力値を配列で受け取るのは以下。

https://docs.djangoproject.com/ja/3.1/ref/request-response/#django.http.QueryDict.getlist