はじめてのDjango (7) 画像データの管理やページへの表示，アップロードの方法などについて知ろう

# はじめに

このドキュメントは，もともとはラボの新メンバー向けの入門テキストとして作成したもので，はじめてDjangoにふれる人にざっとその全体像をつかんでもらうことを狙っています．Djangoの日本語での参考資料も充実してきたので今さらという気がしないでもないですが，見直しを機にQiitaに移すことにしました．万が一でもどなたかの参考になれば幸いです．

説明のための具体例として，Djangoのオフィシャルチュートリアルにある投票アプリをとりあげています（が，説明上の都合で少しコードを追加，変更しているところもあります）．素人の独学がベースで，特に前半は公式チュートリアルをやってみた感想のような記事です（今回は全7回中の最終回）．

全体のコードはまとめて[GitHub](https://github.com/j54854/introDjango)に置きました．

## 変更履歴

- [2021/04/26] Djangoのバーションを3.2に更新し，それ基づいて内容を微修正しました．

# 画像の表示とアップロード

今回は，画像ファイルをデータベースで管理し，ページに表示する方法や，ユーザに画像ファイルをアップロードしてもらう方法をみていきます．

なお，Djangoで画像ファイルを扱うためにはPillowという画像処理ライブラリが必要になるので，まだインストールしていない場合は，下記のコマンドであらかじめインストールしておきましょう．

```console
$ pip install pillow  # pipenvを利用している場合は pipenv install pillow
```

最初に，mysiteのプロジェクト内に，pollsとは別に，albumというアプリケーションを作成します．

```console
$ python manage.py startapp album
```

外側のmysiteのディレクトリで上記のコマンドを打ち込むと，その中にalbumというディレクトリが新たに作成されます（ここで忘れずに，settings.pyの`INSTALLED_APPS`のリストにこのアプリケーションを追加しておきましょう）．

続いて，画像を扱う簡単なモデルを作成します．具体的には，albumディレクトリ内のmodels.py（album/models.py）の中に下記の内容を書き足します．

```python
from django.db import models

class Image(models.Model):
    picture = models.ImageField(upload_to='images/')
    title = models.CharField(max_length=200)

    def __str__(self):
        return self.title
```

これは，画像ファイルを表す`picture`とその表題を表す`title`という2つのfieldをもった，Imageクラスの定義になっています（なお，この`models.ImageField()`を使うためにPillowが必要になるわけです）．

画像ファイルの実体はデータベース内に格納されるわけではなく，あるディレクトリにアップロードされることになります．`models.ImageField()`の引数に指定されている`upload_to`でこのディレクトリを指定しています．

上の例では，`upload_to`にimages/というディレクトリが指定されていますが，これは`MEDIA_ROOT`を起点とした相対パス指定になります．また，`MEDIA_ROOT`内の画像ファイルをurlで参照する際に起点となるアドレスを`MEDIA_URL`として指定しておく必要があります．

今回は，外側のmysiteディレクトリ内にmediaというディレクトリを作成し，それを`MEDIA_ROOT`に指定しましょう．また，`MEDIA_URL`もわかりやすくmediaという名称にしておきます．具体的には，settings.pyの中に下記のように指定しておくことにします．

```python
BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = BASE_DIR.joinpath('media')
MEDIA_URL = '/media/'
```

`BASE_DIR`はデフォルトのままですが，具体的には外側のmysiteディレクトリを指すことになります．`MEDIA_ROOT`はその中のmediaディレクトリという意味にります．また，`MEDIA_URL`をこのように設定すると，開発用サーバを用いている場合，http://localhost:8000/media/ が画像ファイルのurlの起点となります．

次に，このモデルをadminサイトに登録して，adminサイトでImageクラスの画像ファイルを操作できるようにしましょう．まず，album/admin.pyに次の内容を書き込みます．

```python
from django.contrib import admin
from .models import Image

admin.site.register(Image)
```

makemigrationsとmigrateでデータベースの構成を更新してから，開発用サーバを立ち上げ，adminサイトにアクセスしてみましょう．追加したImageクラスが現れているはずです．

これでadminサイトを通じて，画像ファイルをデータベースにアップロードしたり，データベースから削除したりできるようになりました．実際に画像ファイルをいくつかアップロードしてみましょう．

続いて，画像ファイルを表示するページを作成します．このためにまず，album/views.pyの中に次のような簡単なview関数を用意しましょう．

```python
from django.shortcuts import render
from .models import Image

def showall(request):
    images = Image.objects.all()
    context = {'images':images}
    return render(request, 'album/showall.html', context)
```

データベース内にあるすべてのImageクラスのオブジェクトを取得して，それを`images`という名称でtemplateに渡していることがわかります．

次に，templateを用意します．具体的には，album/templates/album/showall.htmlというファイルを作成し，下記の内容を書き込みます．

```html
<h1>Your Album</h1>

{% for image in images %}
    <img src='{{ MEDIA_URL }}{{image.picture}}' width=200>
    <h2>{{ image.title }}</h2>
{% endfor %}
```

albumアプリケーションに関するurlルーティングは，album/urls.pyというファイルを作成し，その中に指定するようにします．ここでは，ひとまず次のように指定しておけばよいでしょう．

```python
from django.urls import path
from . import views

app_name = 'album'

urlpatterns = [
    path('showall/', views.showall, name='showall'),
]
```

また，あわせてmysite/urls.pyの方を次のように更新しておきます．

```python
from django.contrib import admin
from django.urls import include, path
from django.views.generic import base
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('polls/', include('polls.urls')),
    path('album/', include('album.urls')),
    path('admin/', admin.site.urls),
    path('accounts/login/', base.RedirectView.as_view(pattern_name="polls:login")),
    path('accounts/profile/', base.RedirectView.as_view(pattern_name="polls:index")),
] +  static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```

`urlpatterns`の2行目の項目で，album/以降のルーティングを，上で作成したalbum/urls.pyに引き継ぐように指定していることがわかります．

なお，最後に追加されている`static()`は，開発用サーバが，`MEDIA_ROOT`以下のディレクトリに格納されているファイルを`MEDIA_URL`を起点としたurlで供給できるようにするための設定です（詳しくは[ここ](https://docs.djangoproject.com/en/3.2/howto/static-files/)を参照）．

これでOKといいたいところですが，もう1つだけ準備が必要です．settings.pyの`TEMPLATES`の中の`context_processors`に次の項目を追加しておきます（そうでないとtemplate内で`MEDIA_URL`の値が参照できないそうです）．

```python
TEMPLATES = [
    {
        ...
        'OPTIONS': {
            'context_processors': [
                ...
                'django.template.context_processors.media', # これを追加する
            ],
        },
    },
]
```

以上で準備は整ったはずです．http://localhost:8000/album/showall/ にアクセスして画像が表示されることを確認してみましょう．

最後にユーザが画像ファイルをアップロードするためのページを作成しておきましょう．まず，Imageクラスのデータ（画像ファイルとそのタイトル）を取得するためのフォームを作成します．album/forms.pyに次の内容を書き込みます．

```python
from django import forms
from .models import Image

class ImageForm(forms.ModelForm):
    class Meta:
        model = Image
        fields = ['picture', 'title']
```

今回は`ModelForm`という仕組みを利用しました．これは，あるモデルに対応した形式でデータを取得したい場合に，モデルの定義を利用してフォームを簡単に定義する仕組みです（詳細は[ここ](https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/)を参照）．`class Meta`の中をみると，これはImageクラスに対応しており，pictureとtitleのデータを取得するためのフォームであることがわかります．

続いて，album/views.pyの中にアップロード用のview関数`upload()`を用意します．

```python
from django.shortcuts import render, redirect
from .models import Image
from .forms import ImageForm

...

def upload(request):
    if request.method == "POST":
        form = ImageForm(request.POST, request.FILES)
        if form.is_valid():
            form.save()
            return redirect('album:showall')
    else:
        form = ImageForm()

    context = {'form':form}
    return render(request, 'album/upload.html', context)
```

これは，フォームを利用してユーザからデータを取得する場合の典型的なパターンになっていますが，2点だけ補足しておきます．

まず，POSTのリクエストを受けた際に`ImageForm()`に`request.POST`だけではなく，`request.FILES`も渡している点です．これはPOSTで画像（に限らず他の種類であっても）ファイルを取得する際に必要になるので頭の片隅においておきましょう．

2点目は，`form.save()`です．上のImageFormクラスでは特に`save()`メソッドは定義していませんが，`form.ModelForm`を継承しているので，デフォルトの`save()`メソッドが機能します．

具体的には，モデルのインスタンスを渡した場合はそれが取得データで上書きされ，今回のように，インスタンスを渡さなかった場合は新しいインスタンスが生成，保存されることになります（詳しくは[ここ](https://docs.djangoproject.com/en/3.2/topics/forms/modelforms/#the-save-method)を参照）．

簡単なtemplateも用意しておきましょう．album/templates/album/upload.htmlのファイルを作成し，次の内容を書き込みます．

```html
<h1>Please Upload</h1>

<form enctype="multipart/form-data" action="{% url 'album:upload' %}" method="post">
{% csrf_token %}
{{form.as_p}}
<input type="submit" value="Upload">
</form>
```

ここでのポイントは，`<form>`タグの属性として`enctype="multipart/form-data"`を指定しておくことです．この指定がないとファイルをDjangoに渡せません．

最後にurlルーティングの情報をalbum/urls.pyに追加します．具体的には，`urlpatterns`の中に下の記述を追加します．

```python
path('upload/', views.upload, name='upload'),
```

ついでに，showallのページからuploadのページにリンクを張っておきましょう．album/templates/album/showall.htmlの末尾に次の1行を追加します．

```html
<h2><a href="{% url 'album:upload' %}">Upload more</a></h2>
```

以上で準備が整ったので，開発サーバを立ち上げて，http://localhost:8000/album/upload/ にアクセスすると画像のアップロードができるはずです．

# おわりに

以上で最終回まで終了です．　ｍ（＿＿）ｍ