djangoテンプレートの繰り返し処理についてまとめる

今回のお題
-
今回は、djangoテンプレートの繰り返し処理についてまとめます。

繰り返し処理といえばfor文ですが、djagnoテンプレートにはpythonのfor文にはない独自の機能がいくつか追加されています。

これらを使うことで複雑なロジックが簡潔に記述できるようになるので、自分のメモも兼ねてまとめます。

目次
-

- forloop
- for ~ empty
- 回数指定の繰り返し
- for ~ ifchanged
- regroup/grouper
- cycle/resetcycle

forloop
-
forloopはループ変数とも呼ばれる特殊な変数で、繰り返し処理に関する様々な情報を保持しています。

| 1 | 2 |
|:-:|:-:|
| counter  | ループ回数（１スタート）  |
| counter0  | ループ回数(0スタート）  |
| revcounter  | 残りのループ回数（そのループを含む）  |
| revcounter0  | 残りのループ回数（そのループを除く）  |
| first  | 最初のループか否か（真偽値）  |
| last  | 最後のループか否か（真偽値）  |
| parentloop  | 親ループ（入れ子のループの場合にのみ使用可能）  |

これらは属性値なので、`forloop.counter`などとしてループ回数にアクセスします。

```django:forloop使用例
{% for member in members %}
<div>{{ forloop.counter }}人目の登録者は{{ member.name }}さんです。</div>
{% end %}
```

for ~ empty
-
`for ~ empty ~ endfor`のブロックを作ることで、for文で繰り返すリストや辞書が空の場合にempty以下の内容を出力します。

```django:for~empty使用例
{% for member in members %}
<div>{{ member.name }}</div>
{% empty %}
<div>登録されているメンバーはいません</div>
{% endfor %}
```

回数指定の繰り返し
-
djangoテンプレートではfor文の中でrange関数を呼び出すことができません。

その代替案として書籍等でwithブロックの利用が提案されているのを見かけますが、個人的にはあまりしっくり来ませんでした。

withを使ってもブロックのネストは避けられないので、いっそのこと以下のように書けば良いのではないかと思っています。

```django:3回だけ繰り返したい場合
{% for member in members %}
  {% if forloop.counter <= 3 %}
    <div>{{ member.name }}</div>
  {% endif %}
{% endfor %}
```

ifchanged
-
ifchangedブロックで囲んだ部分が前のループと違う値であった場合は出力され、同じ値であった場合には表示されなくなる。

```django:ifchanged
<table>
      <thead>
        <tr>
          <th>コード</th>
          <th>書名</th>
        </tr>
      </thead>
      {% for book in books %}
        <tr>
          <td>
            {% ifchanged  %}
            {{ book.isbn }}
            {% endifchanged %}
          </td>
          <td>
            {% ifchanged  %}
            {{ book.title }}
            {% endifchanged %}
          </td>
        </tr>
    {% endfor %}
</table>
```
上記のように書くとコードが一つ前の書籍と同じ場合には出力されなくなります（以下参照）。
実際には違う本でコードが同じなんてことはありえませんが。

[![Image from Gyazo](https://i.gyazo.com/a95404d5c362d9d434a4d66952346e04.png)](https://gyazo.com/a95404d5c362d9d434a4d66952346e04)

また、以下のようにelseブロックを作ることで前回のループと値が同じ場合の出力を決めることもできますが、elifに相当するブロックはないようです。

```django
{% ifchanged  %}
  {{ book.isbn }}
{% else %}
  同上
{% endifchanged %}
```

[![Image from Gyazo](https://i.gyazo.com/c8a15add5f1ccae998579f3b54599e30.png)](https://gyazo.com/c8a15add5f1ccae998579f3b54599e30)
先ほどまで空欄だった部分に`同上`の文字が入る。

regroup/grouper
-
regroupは、あるリストから特定の属性をキーとして新しいグループを作ります。

```django:regroupの例
{% regroup members by age as sorted %}
```
例えば上記の例ではmembersというリスト(Membersモデルオブジェクトのリストだと思ってください)をage属性の値でグループ化しています。

このときに作成される`sorted`という変数の中身は以下のようになっており、

```python
# regroupされるmembers
members = [
  { "name": "taro", "age": 30 },
  { "name": "jiro", "age": 30 },
  { "name": "hanako", "age": 30 },
  { "name": "keji", "age": 30 },
  { "name": "yumi", "age": 25 },
  { "name": "sigeru", "age": 25 },
]

# 新しく作られた配列sorted
sorted = [
  # age = 30のグループ
  { "grouper": 30,
    "list": {
        { "name": "taro", "age": 30 },
        { "name": "jiro", "age": 30 },
        { "name": "hanako", "age": 30 },
        { "name": "keji", "age": 30 },
    }
  },
  # age = 25のグループ
  { "grouper": 25,
    "list": {
        { "name": "yumi", "age": 25 },
        { "name": "sigeru", "age": 25 },
    }
  }
]
```
少し分かりにくいですが、sortedは辞書を要素とするリストです。

要素の数はregroupのキー（今回はage)の値の数（今回は30と25なので2つ）になります。

また、sortedの中の一つ一つの辞書は`grouper`と`list`の二つのキーをもち、`grouper`に対応する値にはageの値(30ないしは25)が与えられ、`list`にはageが30ないしは25のオブジェクト全てのリストが与えられます。

cycle/resetcycle
-
cycleは、指定した値を順番に出力するためのものです。

例えば、

```django
{% cycle "red" "blue" "green" "yellow" %}
```
とすることで`red`, `blue`, `green`, `yellow`が順番に出力されます。

また、`cycle`はhtml要素の属性値としても使えるので、

```django
{% for book in books %}
      <div class="text-{% cycle 'danger' 'primary' 'success' %}">{{ book.title }}</div>
{% endfor %}
```
とすると
[![Image from Gyazo](https://i.gyazo.com/21c6ddd05db871defec043fb0bb68b42.png)](https://gyazo.com/21c6ddd05db871defec043fb0bb68b42)
となります(BootStrapを使いました）。

このcycleタグは同一のforループ内では何度呼び出しても同じ値を取るのですが、呼び出すたびにcycle以下を全て書くのは大変なので、`as`で別名をつけて使い回すと楽になります。

```django
<!-- 以下の2つは同じ結果になる -->
<!-- as 未使用 -->
{% for book in books %}
      <div class="text-{% cycle 'danger' 'primary' 'success' %}">{{ book.title }}</div>
      <div class="text-{% cycle 'danger' 'primary' 'success' %}">{{ book.isbn }}</div>
{% endfor %}
<!-- as　を使用 -->
{% for book in books %}
      <div class="text-{% cycle 'danger' 'primary' 'success' as bg %}">{{ book.title }}</div>
      <div class="text-{{ bg }}">{{ book.isbn }}</div>
{% endfor %}
```
結果
[![Image from Gyazo](https://i.gyazo.com/821df4e048f9ae4a0b478e3d66d96992.png)](https://gyazo.com/821df4e048f9ae4a0b478e3d66d96992)

また、resetcycleはcycleの繰り返しをリセットするために使います。

```django
{% regroup sorted by isbn as books_isbn %}

      {% for book_isbn in books_isbn %}
        <div>{{ book_isbn.grouper }}のグループ</div>
        {% for b in book_isbn.list %}
          <div class="text-{% cycle 'danger' 'primary' 'info' %}">{{ b.title }}</div>
        {% endfor %}
        {% resetcycle %}<!-- ここでcycleがリセットされる -->
      {% endfor %}
```
先程のregroupと併用しています。

結果がこちら。
[![Image from Gyazo](https://i.gyazo.com/6320ba4ab4e3dc150f550c8c42c83b4b.png)](https://gyazo.com/6320ba4ab4e3dc150f550c8c42c83b4b)
基本的には赤、青、水色のループですが、テスト３とテスト４は同じ色になっていますね。

これはyyy-abcdのグループからzzz-zzzzのグループにループが移るときに`resetcycle`をまたぐのでcycleの値が初期化されるためです。

終わりに
-
以上で繰り返し処理に関するお話を終わります。

djangoには条件分岐処理などについても特有の仕様があるので、そちらについてもいずれまとめたいですね。

追記：条件分岐処理についてもまとめました。

https://qiita.com/keishi04hrikzira/items/4def8eec174fce7cfd9b