Djangoの個人的Tips

この記事は[Django Advent Calendar 2019](https://qiita.com/advent-calendar/2019/django) の最終日の記事です。


# はじめに
はじめまして。よろしくお願いします。

普段からDjangoを使用してWebアプリを開発しています。

今年の[Django Advent Calendar 2019](https://qiita.com/advent-calendar/2019/django) も毎日、楽しく拝見させて頂きました。
興味深く、勉強になる記事ばかりでした。
そこで本カレンダーの最終日であるこの記事ではDjangoのいくつかの基本的な、そして個人的なTipsを整理したいと思います。
この記事がDjangoのより一層の普及に少しでも貢献できれば幸いです。


# 目次
 - [フォームのバリデーション](#フォームのバリデーション)
 - [Http404 を送出する](#http404を送出する)
 - [インスタンス取得を実行し、オブジェクトが存在しない場合は Http404 を送出する](#インスタンス取得を実行しオブジェクトが存在しない場合は-http404-を送出する)
 - [時間のかかる処理をPickleを使って事前に行う](#時間のかかる処理をpickleを使って事前に行う)
 - [時間のかかる処理をModelを使って事前に行う](#時間のかかる処理をmodelを使って事前に行う)
 - [ログイン認証機能を使う](#ログイン認証機能を使う)
 - [Modelに格納されているデータを、ログインしているユーザー毎に取得する](#modelに格納されているデータをログインしているユーザー毎に取得する)
 - [OR条件でオブジェクトを抽出する](#or条件でオブジェクトを抽出する)
 - [Templateでループカウンタを取得する](#templateでループカウンタを取得する)
 - [Templateでlistの配列番号を指定する](#templateでlistの配列番号を指定する)
 - [(番外編)画像ストレージとしてCloudinaryを使う](#番外編画像ストレージとしてcloudinaryを使う)


# フォームのバリデーション
Webの入力画面から値を受け取って、それをModelにセットする時、不正な値が入ってないかを検証することができます。
Django標準のis_valid関数を使用することで、例えば、IntegerFieldの項目に数値以外のものが入った場合や、必須の項目が空欄だった場合にErrorを送出させることができます。

```python
if form.is_valid():  # フォームのバリデーション
    # 処理
    # ...
```


# Http404を送出する
アクセスするURL毎に引数を受け取って処理を分岐させる場合は、elseで`raise Http404`を構えておくと良いでしょう。
以下のようにしておけばこの関数において想定外のURLで引数が渡された時に`Server Error (500)`を回避できます。

```python:views.py
from django.http import Http404

def sample(request, mode):
    if mode == 'hoge':
        # 処理1
    elif mode == 'fuga':
        # 処理2
    else:
        raise Http404("No User matches the given query.")
```


# インスタンス取得を実行し、オブジェクトが存在しない場合は Http404 を送出する

クエリを発行してModelからインスタンスを取得する際には`get_object_or_404`を使用すると良いかもしれません。
以下の例では、`MyModel`から主キーが 1 のオブジェクトを取得していますが、もし該当するオブジェクトが存在しなければ Http404 を送出します。
このようにすることで不用意に`Server Error (500)`を起こさずに済むはずです。

```python:views.py
from django.shortcuts import get_object_or_404

def my_view(request):
    my_object = get_object_or_404(MyModel, pk=1)
```


# 時間のかかる処理をPickleを使って事前に行う
Viewで大きな処理を行う時、処理に時間がかかってしまい、レスポンスタイムの増大を引き起こすことがあるでしょう。
そのようなときには`pickle.dump`と`pickle.load`を使うと良いかもしれません。
前処理として途中まで処理を行い、その結果のPythonオブジェクトをpickle化しておいて、本処理ではそれをloadして使いましょう。
そうすることでレスポンスタイムは大きく向上します。

```python:
# このような関数を通してPyhtonオブジェクト -> pickleにする
def pickle_dump(obj, path):
    with open(path, mode='wb') as f:
        pickle.dump(obj,f)

# 本処理から使う時はpickle -> pythonオブジェクトに戻す
def pickle_load(path):
   with open(path, mode='rb') as f:
       data = pickle.load(f)
       return data
```

あとは前処理を定期的にジョブスケジューラーで呼び出して、自動化しておくと良いでしょう。


# 時間のかかる処理をModelを使って事前に行う
あなたがHerokuを使って、DjangoProjectを本番運用している場合、上記の方法を試そうとすると上手くいかないかもしれません。
それはHerokuのファイルシステムが一時的なものであるという[仕様](https://devcenter.heroku.com/articles/active-storage-on-heroku)によるものです。
せっかく処理の結果をpickle.dumpしても、あなたのDjangoProject上にpickleは保存されません。

そのようなときはPythonオブジェクトをModelに格納するといいかもしれません。

そのままPythonオブジェクトをModelに格納するには[カスタムのモデルフィールド](https://djangoproject.jp/doc/ja/1.0/howto/custom-model-fields.html)を用意する必要があります。
ただ、list型のPythonオブジェクトであればjson形式にすることでDjango標準のCharFieldを使えるでしょう。

```python:models.py
class Pickle(models.Model):
    sample = models.CharField(max_length=255, null=True)

    def set_sample(self, x):
        self.sample = json.dumps(x)

    def get_sample(self):
        return json.loads(self.sample)
```

上記のようにModelクラスメソッドを用意して、setterもしくはgetterを通して、Modelを操作します。
このようにjsonとCharFieldを上手く使うことで多くのケースに対応できるはずです。
例えば、pandas.DataFrame -> numpy.ndarray -> list -> json と型変換することでpandas.DataFrameもModelに格納できます。


# ログイン認証機能を使う
Webアプリケーションを作成する上でログイン認証機能を利用することは多いでしょう。

Djangoでは標準機能としてログイン認証が用意されています。
これらを使うことでさくっとログイン認証を実装することができます。
[公式ドキュメント](https://docs.djangoproject.com/ja/3.0/topics/auth/)にも説明があります。

認証機能の実装は3ステップです。

1. 認証機能を使う為に以下のコマンドでDjangoProjectにappを作成します。

```
$ django-admin startapp accounts
```

2 . settings.pyで作成したappを追加します。

```
INSTALLED_APPS = [
    ...
    'accounts.apps.AccountsConfig', # 追加
]
```

3 .  ログイン成功後に遷移するURLもsettings.pyで定義します。

```
# ログイン後トップページにリダイレクト
LOGIN_REDIRECT_URL = '/hoge/'
```

以上でログイン認証機能の実装は完了です。

そして、Viewの関数に```@login_required```を付けることでログイン認証によるアクセス制限が可能となります。

```
@login_required
def sample(request):
    pass
```

ちなみに、ユーザー登録機能はDjangoの標準機能では用意されていませんが、自分で作ることができます。
また、その他にログイン画面、ログアウト画面をカスタマイズすることもできます。

参考として、以下にユーザー登録機能実装のコードを記載しておきます。

```python:accounts/urls.py
urlpatterns = [
    path('signup/', views.SignUpView.as_view(), name='signup'),
]
```

```python:accounts/views.py
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from django.views import generic


class SignUpView(generic.CreateView):
    form_class = UserCreationForm
    success_url = reverse_lazy('login')
    template_name = 'accounts/signup.html'
```


# Modelに格納されているデータを、ログインしているユーザー毎に取得する

ログイン認証を実装すると、Modelに格納されている全オブジェクトから、ログインしているユーザー毎にオブジェクトを抽出したい要件がきっと出てくるでしょう。
そのような時はfilter関数で以下のように条件を設定するとログインしているユーザーのオブジェクトのみを抽出することができます。

```python:views.py
from models import Content

@login_required
def sample(repuests):
    contents = Content.objects.all().filter(user_id=request.user.id)
```


# OR条件でオブジェクトを抽出する
DjangoでModelからオブジェクトを抽出する場合、AND条件はfilter関数を重ねれば良いのですが、OR条件は`Q`ライブラリを使うと良いでしょう。
例えば、以下のように記述することで日付が`2019-12-24` または `2019-12-25` または `2019-12-26` のオブジェクトのみを抽出することができます。

```python:views.py
from django.db.models import Q
from models import Content

def sample(request):
    contents = Content.objects.filter(
                Q(date='2019-12-24') | Q(date='2019-12-25') | Q(date='2019-12-26')
                )
```


# Templateでループカウンタを取得する
Viewから渡されたlistをTemplateの中のforで回すとき、
以下のように実装するとそのforの中でのループカウンタを取得できます。

```html
{% for list in lists %}
    {{ forloop.counter }}
{% endfor %}
```

さらに`counter`の部分を`counter0`(0から順)や`revcounter`(逆順)に置き換えると様々なループカウンタを取得することができます。


# Templateでlistの配列番号を指定する
Template内でもViewから渡されたlistに対して
Pythonと同じように配列番号を指定して要素を取り出すことができます。

```html
{{ objects.0 }}
```


# (番外編)画像ストレージとしてCloudinaryを使う
これはTipsではないのですが、私はふだん画像ストレージとしてCloudinaryを使用しています。

[時間のかかる処理をModelを使って事前に行う](#時間のかかる処理をmodelを使って事前に行う)でも触れた通り、Heroku上で稼働するDjangoProjectにファイルを保存することはできません。
そこでHeroku上のDjangoアプリケーションで画像をアップロードしたり、アップロードした画像を管理したいときは、画像用ストレージを用意する必要があります。

Cloudinaryを使用することでDjangoアプリケーションで画像データを扱うことが出来ます。

[Cloudinary公式ドキュメント](https://devcenter.heroku.com/articles/cloudinary#using-with-django)

Cloudinaryを使うために必要なことは以下の3つです。

 - Model定義
 - settings.pyの設定
 - Heroku Add-onsの追加


```python:models.py
from cloudinary.models import CloudinaryField

class Image(models.Model):
    picture = CloudinaryField('picture')
```

```python:settings.py
# Add
CLOUDINARY_STORAGE = {
    'CLOUD_NAME': '*****',
    'API_KEY': '*****',
    'API_SECRET': '*****'
}

# ...

# Add
DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage'
```

上記のように設定すればあとは`ImageField`と同じように使えます。


# さいごに
最後まで読んで下さり、ありがとうございます。
いかがだったでしょうか。
もし他にも「こういうやり方もあるよ」などあればコメント頂けると幸いです。

それでは良いDjangoライフを！