Djangoでのドロップダウンメニューの実装

#実装イメージ
![スクリーンショット 2020-08-17 14.54.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362091/26638204-f7c4-97a4-1ed7-1c85411ba90f.png)

こんな感じで文字をクリックすると下層のアイテムが表示されるようなものを作る。(上のやつにはチェックボックスがついてますが、とりあえずはチェックボックスなしでDBのデータが階層構造で表示されることを目指す。)

#実装1(Date)
まずは使用するデータを準備する。今回は画像のように親カテゴリ→子カテゴリ→孫カテゴリとなるようにDBとデータの準備を行う。models.pyに以下を追加。

```python:models.py
class CategoryIDModels(models.Model):
    class Meta:
        db_table = 'CategoryID'

    CategoryID = models.CharField(
        primary_key=True,
        verbose_name='CategoryID',
        blank=False,
        null=False,
        max_length=20,
        default='',
    )

    Category_name = models.CharField(
        verbose_name='カテゴリ名',
        blank=False,
        null=False,
        max_length=225,
        default='',
    )
class CategoryID2Models(models.Model):
    class Meta:
        db_table = 'CategoryID2'

    CategoryID = models.CharField(
        primary_key=True,
        verbose_name='CategoryID',
        blank=False,
        null=False,
        max_length=20,
        default='',
    )

    Category_name = models.CharField(
        verbose_name='カテゴリ名',
        blank=False,
        null=False,
        max_length=225,
        default='',
    )

    ParentCategoryID = models.ForeignKey(
        CategoryIDModels, 
        to_field='CategoryID',
        verbose_name='親カテゴリ',
        on_delete=models.CASCADE,
        null=True
    )
class CategoryID3Models(models.Model):
    class Meta:
        db_table = 'CategoryID3'

    CategoryID = models.CharField(
        primary_key=True,
        verbose_name='CategoryID',
        blank=False,
        null=False,
        max_length=20,
        default='',
    )

    Category_name = models.CharField(
        verbose_name='カテゴリ名',
        blank=False,
        null=False,
        max_length=225,
        default='',
    )

    ParentCategoryID = models.ForeignKey(
        CategoryID2Models, 
        to_field='CategoryID',
        verbose_name='親カテゴリ',
        on_delete=models.CASCADE,
        null=True
    )
```

CategoryIDmodels（親）→CategoryID２models（子）→CategoryID３models（孫）としてCategoryID２と３は上位層の「CategoryID」カラムをForeignKeyとして設定している。

投入するデータとしては簡単に以下。
![スクリーンショット 2020-08-20 10.42.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362091/ed7a1e9f-0f08-d02a-68f5-f08d316f091b.png)

またデータ投入する際にCategoryID2ModelsとCategoryID3ModelsはParentCategoryIDカラムも設定すること。
ForeignKeyについては公式ドキュメントや以下を参照。

[Django ForeignKeyで1対多のモデルを構築](https://noumenon-th.net/programming/2019/12/02/foreignkey/)



#実装2(HTML)
次にhtml側の準備を行う。プロジェクトのtemplatesフォルダ配下に「Dropmenu.html」を作成する。

```html:Dropmenu.html
<!DOCTYPE html>

{% load static %}
{% load Drop %}

<html lang="ja">
    <head>
      <meta charset="utf-8">
      <link rel="stylesheet" type="text/CSS" href="{% static 'css/drop.css' %}" />
      <title>{% block title %}DropMenu{% endblock %}</title>
    </head>
    <body>
        {% csrf_token %}
        {% for data in form %}
        <span><p id="click_event" style="display:inline;">{{data.Category_name}}</p></span>
              <ul>
                {% for things in form_child|in_category:data.CategoryID %}
                <li><p id="click_event2" style="display:inline;">{{things.Category_name}}</p>
                    <ul>
                    {% for thing in form_gchild|in_category:things.CategoryID %}
                      <li>{{thing.Category_name}}</li>
                    {% endfor %}
                    </ul>
                </li>
                {% endfor %}
              </ul>
        {% endfor %}
            <script type="text/javascript">
              $(function () {
                // 親メニュー処理
                $(document).on('click', '#click_event', function(){
                  $(this).parent().next('ul').slideToggle('fast');
                });
                // 子メニュー処理
                $(document).on('click', '#click_event2', function(e){
                  $(this).parent().children('ul').slideToggle('fast');
                  e.stopPropagation();
                });
              });
            </script>
    </body>
</html>

```

CSSについては以下、デザインについては好きなようにいじって頂きたい。

```css:drop.css
span {
    display: block;
    margin: 0 0 4px 0;
    padding : 15px;
    line-height: 1;
    color :#fff;
    background: #5200b7;
    cursor :pointer;
}
  
li {
    cursor: pointer;
    border-bottom: 1px solid #5200b7;
    color: #222;
}
```

今回の実装で重要な部分として「カスタムテンプレートフィルタ」を作成している。
HTMLファイルで３行目{% load Drop %}がそのカスタムテンプレートフィルタを読んでいる処理である。
カスタムテンプレートフィルタについては細かい説明はしないがDjangoのテンプレートで使用できるフィルタを自作したものである。
作成方法として、プロジェクト内に「templatetags」フォルダを作成し、その中にカスタムテンプレートファイルを作成するだけである。

以下がカスタムテンプレートファイルである。

```python:Project_folder/templatetags/Drop.py
register = template.Library()

@register.filter
def in_category(things, category):    
    return things.filter(ParentCategoryID=category)
```

処理内容として、第一引数でDBデータ、第二引数でCategoryIDを受け取る。受け取ったDBデータに対してParentCategoryIDでフィルターをして返すといったシンプルなものである。

使い方はhtmlファイルの１７行目

```html:
{% for things in form_child|in_category:data.CategoryID %}
```

form_childというDBデータに対しカスタムテンプレートフィルタを適用している。第二引数にはCategoryIDModelsのCategoryIDを渡している。

このままだと若干わかりにくいのでviews.pyでtemplateに渡しているデータをみてみよう。

#実装３(views)

```python:views.py
@login_required
def Dropmenu_date(request_val):
    ## テンプレート読み込み
    template = loader.get_template('Dropmenu.html')
    form = CategoryIDModels.objects.all().order_by('CategoryID')
    form_child = CategoryID2Models.objects.all().order_by('CategoryID')
    form_gchild = CategoryID3Models.objects.all().order_by('CategoryID')

    context = {
        'form': form,
        'form_child': form_child,
        'form_gchild': form_gchild,
    }

    return HttpResponse(template.render(context, request_val))
```

あとはurls.pyでviewsとtemplateの紐付けを行えばドロップダウンメニューが表示されるはずだ。