【 Docker＋Nginx＋Django＋RDS】WEBアプリができるまで⑧画像ファイルのアップロード


# 前置き
独学で、子供の成長アプリを作った時のことを、記録として残していきます。
間違っているところなどあれば、ご連絡お願いします。
　①[Djangoのようこそページへたどり着くまで](https://qiita.com/quantity82/items/e8c2392bf982efb55f80)
　②[NginxでDjangoのようこそページへたどり着くまで](https://qiita.com/quantity82/items/88a6348135e25c661feb)
　③[カスタムユーザーを作ってadminにたどり着く](https://qiita.com/quantity82/items/b3cd6ded5bbb5f3726f3)
　④[ログインログアウトをしよう](https://qiita.com/quantity82/items/8e4b7ce2ef56e60ad392)
　⑤[ユーザー登録（サインイン）機能を作ろう](https://qiita.com/quantity82/items/bbfc7e72d4a65c71e24c)
　⑥[ユーザーごとのデータ登録できるようにする〜CRU編](https://qiita.com/quantity82/items/4d019a4ee475208a5a26)
　⑦[ユーザーごとのデータ登録できるようにする〜削除編](https://qiita.com/quantity82/items/c48fee71b5f8eb766dae)
　⑧[画像ファイルのアップロード](https://qiita.com/quantity82/items/9bcbd25902f6878ee325)　<--ここです
　⑨[身長体重を記録する＠一括削除機能つき](https://qiita.com/quantity82/items/c8e58742eeaa8c0fc954)
　⑩[成長曲線グラフを描いてみよう](https://qiita.com/quantity82/items/619f5ed2a6cd45f7f592)
　⑪[本番環境へデプロイ＋色々手直し](https://qiita.com/quantity82/items/6e91b7ee7a2d7e0fab32)




# Goal
子供の靴のサイズなどを記録できる機能を作ります。
このへんから、実際のサービスっぽくなります。

# 作るもの
子供達の靴の情報を登録・管理する機能を、アプリ分割します。

```
docker-compose run web python ./manage.py startapp shoes
```
```
.
├── docker-compose.yml
├── nginx
│   ├── conf
│   │   └── mysite_nginx.conf
│   └── uwsgi_params
├── src
│   ├── manage.py
│   ├── media
│   │   └── image
│   │       └── noimage.png
│   ├── mysite
│   │   ├── __init__.py
│   │   ├── __pycache__
│   │   │   └── 略
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── shoes
│   │   ├── __init__.py
│   │   ├── __pycache__
│   │   │   └── 略
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── forms.py
│   │   ├── migrations
│   │   │   └── 略
│   │   ├── models.py
│   │   ├── tests.py
│   │   ├── urls.py
│   │   └── views.py
│   ├── static
│   │   └── 略
│   ├── templates
│   │   ├── base.html
│   │   ├── shoes
│   │   │   ├── shoes_data_add.html
│   │   │   ├── shoes_data_edit.html
│   │   │   └── shoes_data_list.html
│   │   └── users
│   │       ├── kids_profile_add.html
│   │       ├── kids_profile_edit.html
│   │       ├── login.html
│   │       ├── mypage.html
│   │       └── signup.html
│   └── users
│       ├── __init__.py
│       ├── __pycache__
│       │   └── 略
│       ├── admin.py
│       ├── apps.py
│       ├── forms.py
│       ├── migrations
│       │   └── 略
│       ├── models.py
│       ├── tests.py
│       ├── urls.py
│       └── views.py
├── static
│       └── 略
└── web
    ├── Dockerfile
    └── requirements.txt
```

# Model
靴情報は子供ごとに持つので、userとKidsProfileのモデルを外部参照。

靴のサイズは一般的に0.5ずつ上がるものなので、range関数を使って選択肢を生成。
ただしrange関数は整数しか扱えないので、うまいことやってあげる必要あり。

そして靴の画像を撮らないと、なんの靴のことを記録してるのかわからないので
画像をアップロードできるようにします。ImageField。ただしアップロードしない
人もいるかもしれないので、そんなときのためにdefaultの画像はこちらで用意。

```shoes/models.py
from django.db import models
from django.conf import settings
from django.utils import timezone
from users.models import KidsProfile

#靴データ
class ShoesData(models.Model):

    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,null=True)
    kidsProfile = models.ForeignKey(KidsProfile, on_delete=models.CASCADE,null=True)
    buy_date = models.DateField(default=timezone.now)
    shoes_size = models.DecimalField(
                max_digits=3,
                decimal_places=1,
                choices = ((x/2, str(x/2)) for x in range(16,50,1)),
                blank = False
                )
    shoes_memo = models.TextField(blank=True, null=True)
    shoes_image = models.ImageField(upload_to="image/", blank=True, default="image/noimage.png")

    def __str__(self):
        return self.kidsProfile.name + str(self.buy_date)
```

# Form
モデルに合わせてFormも作ります。
追加するときや参照するときは子供の情報が必要なのですが、
編集するときは「どの子の情報を直すか」は分かっているので、Edito用のFormも作ります。
なんだか冗長な気がしましたが、これ以上のやり方もわからず。

```forms.py
from django import forms
from .models import ShoesData
from users.models import KidsProfile
import bootstrap_datepicker_plus as datetimepicker


class ShoesDataForm(forms.ModelForm):
    class Meta:
        model = ShoesData
        fields = ('kidsProfile', 'buy_date', 'shoes_size', 'shoes_memo', 'shoes_image')
        widgets = {
            'buy_date': datetimepicker.DatePickerInput(
                           format='%Y-%m-%d'),
        }

    def __init__(self, *args, **kwargs):
        user = kwargs.pop('user','')
        super(ShoesDataForm, self).__init__(*args, **kwargs)
        self.fields['kidsProfile'] = forms.ModelChoiceField(queryset=KidsProfile.objects.filter(user=user))

class ShoesDataEditForm(forms.ModelForm):
    class Meta:
        model = ShoesData
        fields = ('buy_date', 'shoes_size', 'shoes_memo', 'shoes_image')
        widgets = {
            'buy_date': datetimepicker.DatePickerInput(
                           format='%Y-%m-%d'),
        }

    def __init__(self, *args, **kwargs):
        user = kwargs.pop('user','')
        kidsProfileId = kwargs.pop('kidsProfileId','')
        super(ShoesDataEditForm, self).__init__(*args, **kwargs)
```

# 画像を扱おう（準備）

扱うために３つほど導入します。

・pillow
ImageFieldを扱うためのは、pillowというライブラリが必要です。


・django-cleanup
ImageFieldは、あくまでImageFieldオブジェクトなので、実際のjpegとかpngを管理はしてくれません。
つまり、ImageFieldを削除しても、画像ファイルは削除してくれないのです。（ゴミが残り続ける！）
そんなときのために、django-cleanupというライブラリを使います。

・Lightbox
画像をぽわーんとそれっぽく表示するやつです。
必要なCSSとJSは、STATICフォルダに格納しておいてください。

```requirement.txt
Django==2.2.2
psycopg2==2.8.4
uwsgi==2.0.17
django-bootstrap4==1.1.1
django-bootstrap-datepicker-plus==3.0.5
pillow==7.0.0 #追加
django-cleanup==4.0.0 #追加
```

ImageFieldで指定したupload_toは、setting.pyで指定したMEDIAフォルダに行きます。
setting.pyにも、いろいろ追加しましょう。

```setting.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users.apps.UsersConfig',
    'shoes.apps.ShoesConfig', #追加
    'bootstrap4',
    'bootstrap_datepicker_plus',
    'django_cleanup.apps.CleanupConfig', #追加
]

（略）

MEDIA_ROOT = os.path.join(BASE_DIR, 'media') #追加
MEDIA_URL = '/media/' #追加
```

# HTML/CSS
一覧表示するTemplate。モーダルを使って削除情報出すのは、前回と同様。
ポイント
（shoes_data_list）
・子供情報はページ左部にlist-group形式で並べて、選択するとそのIDを持たせて再表示。
　urls.py（とview.py）では、IDがくるときと、IDがこないときで待ち受け。
・画像ファイルは、登録がなかったらno-imageを表示させる。
・画像ファイルはbackground-imageで扱うことで、レスポンシブ対応に。
　画像サイズのレスポンシブ設定はCSSにて。

（shoes_data_list）
・Lightboxとdatepicker用のブロックコメントの順番は、これじゃないとダメ。
　datepickerのポップアップが機能しなくなります。（結構ハマった）
・メニューもちゃんとリンクを貼るようにしてみた。
　アプリを分けているので、ちゃんと名前指定（shoes:みたいに）しないとダメ。

（shoes_data_add,shoes_data_edit）
ひねりなし。言うこともなし。

```shoes/shoes_data_list.html
{% extends 'base.html' %}

{% block extra_js %}
<script>
$(function() {
  $('.del_confirm').on('click', function () {
     $("#del_pk").text($(this).data("pk"));
     $('#del_url').attr('href', $(this).data("url"));

     $(this).attr('href', href);
  });
});
</script>
{% endblock extra_js %}

{% block content %}
<div class="row">
    <div class="col-md-12 col-lg-2">
        <div class="list-group">
          {% for kidsProfile in kidsProfiles %}
            <a href="/shoes/list/{{kidsProfile.id}}" class="list-group-item list-group-item-action">{{kidsProfile.name}}</a>
          {% endfor %}
        </div>
    </div>
    <div class="col-md-12 col-lg-10 overflow-auto">

        <div class="card">
            <div class="card-header">
                {{ kidsName }}
            </div>

            <div class="card-body">
                <div class="container-fluid">
                    {% for shoes_data_post in shoes_data_posts %}
                    <div class="row">
                        {% if shoes_data_post.shoes_image %}
                            <a href="{{ shoes_data_post.shoes_image.url }}"
                               style="background-image: url({{ shoes_data_post.shoes_image.url }})"
                               class="col-4 col-lg-2 border shoes_img" data-lightbox="demo"></a>
                        {% else %}
                            <a href="/media/image/noimage.png"
                               style="background-image: url(/media/image/noimage.png)"
                               class="col-4 col-lg-2 border shoes_img" data-lightbox="demo"></a>
                        {% endif %}
                        <div class="col-8 col-lg-10 pl-lg-5">
                            <div class="row">
                                <div class="col-md-12 col-lg-2 mb-2 rounded bg-secondary">名前</div>
                                <div class="col-md-12 col-lg-3 mb-2">{{ shoes_data_post.kidsProfile }}</div>
                                <div class="col-md-0  col-lg-7 "></div>
                                <div class="col-md-12 col-lg-2 mb-2 rounded bg-secondary">購入日</div>
                                <div class="col-md-12 col-lg-3 mb-2">{{ shoes_data_post.buy_date }}</div>
                                <div class="col-md-0  col-lg-1 "></div>
                                <div class="col-md-12 col-lg-2 mb-2 rounded bg-secondary">サイズ</div>
                                <div class="col-md-12 col-lg-3 mb-2">{{ shoes_data_post.shoes_size }}cm</div>
                                <div class="col-md-0  col-lg-1 "></div>
                                <div class="w-100"></div>
                                <div class="col-lg-2 rounded bg-secondary">メモ</div>
                                <div class="col-lg-10">{{ shoes_data_post.shoes_memo }}</div>
                                <div class="col-12 text-right mt-2">
                                    <a href="{% url 'shoes:shoes_data_edit' shoes_data_post.id %}" role="button" class="btn btn-success btn-sm">編集</a>
                                    <button type="button" class="btn btn-danger btn-sm del_confirm" data-toggle="modal" data-target="#delete_modal"
                                        data-pk="{{ shoes_data_post.pk }}" data-url="{% url 'shoes:shoes_data_delete' shoes_data_post.id %}">削除</button>
                                </div>
                            </div>
                        </div>
                    </div>
                    <hr>
                    {% endfor %}
                </div>
            </div>
        </div>
        <a href="{% url 'shoes:shoes_data_add' %}" role="button" class="btn btn-primary mr-auto">追加</a>


        <div class="modal fade" id="delete_modal" tabindex="-1" role="dialog" aria-labelledby="label1" aria-hidden="true">
            <div class="modal-dialog modal-dialog-centered">
                <div class="modal-content">
                    <div class="modal-body">
                        削除しても良いですか？
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">キャンセル</button>
                        <a href="#" id="del_url" class="btn btn-danger">削除</a>
                    </div>
                 </div>
             </div>
        </div>
    </div>
{% endblock %}

```
```base.html
{% load static %}

<html>
    <head>
        <title>kids Growth</title>

        <link rel="stylesheet" href="{% static 'css/kidsGrowth.css' %}">
        <link rel="stylesheet" href="{% static 'css/lightbox.min.css' %}">
        <script src="{% static 'js/lightbox-plus-jquery.min.js' %}"></script>
        {% block extra_js %}{% endblock %}
        {% load bootstrap4 %}
        {% bootstrap_css %}
        {% bootstrap_javascript %}
    </head>
    <body>
        <!-- Navigation -->
        <nav class="navbar navbar-expand-sm navbar-dark bg-dark mt-3 mb-3">
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav4" aria-controls="navbarNav4" aria-expanded="false" aria-label="Toggle navigation">
              <span class="sr-only">メニュー</span>  
              <span class="navbar-toggler-icon"></span>
            </button>
            <a class="navbar-brand" href="">Kids Growth</a>
            <div class="collapse navbar-collapse" id="navbarNav4">
                <ul class="navbar-nav">
                  <li class="nav-item">
                      <a class="nav-link" href="">メニュー１</a>
                  </li>
                  <li class="nav-item">
                      <a class="nav-link" href="">メニュー２</a>
                  </li>
                  <li class="nav-item">
                      <a class="nav-link" href="{% url 'shoes:shoes_data_list'%}">シューズ一覧</a>
                  </li>
                  <li class="nav-item">
                      <a class="nav-link" href="{% url 'users:mypage'%}">マイページ</a>
                  </li>
                  {% if user.is_authenticated %}
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'users:logout'%}">ログアウト</a>
                    </li>
                  {% else %}
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'users:login'%}">ログイン</a>
                    </li>
                  {% endif %}
                </ul>
            </div>
        </nav>

        <div class="content container">
		  {% block content %}
          {% endblock %}
        </div>
    </body>
</html>
```
```shoes/shoes_data_add.html
{% extends 'base.html' %}

{% block content %}

<!--日付入力フォーマット用-->
{{ form.media }}

<div class="col-md-12 col-lg-5">
    <h2>New data</h2>
    <form method="POST" class="post-form"  enctype="multipart/form-data">{% csrf_token %}
        {% bootstrap_form form %}
        <button type="submit" class="save btn btn-primary">Save</button>
    </form>
</div>
{% endblock %}
```
```shoes/shoes_data_edit.html
{% extends 'base.html' %}

{% block content %}

<!--日付入力フォーマット用-->
{{ form.media }}

<div class="col-md-12 col-lg-5">
    <h2>Data Edit</h2>
    <form method="POST" class="post-form"  enctype="multipart/form-data">{% csrf_token %}
        {% bootstrap_form form %}
        <button type="submit" class="save btn btn-primary">Save</button>
    </form>
</div>
{% endblock %}
```
```kidsGrowth.css
.shoes_img {
  background-size: cover;
  background-position: center center;
}
```
```shoes/urls.py
from django.urls import path
from . import views

app_name = 'shoes'

urlpatterns = [
    path('shoes/list/', views.shoes_data_list, name='shoes_data_list'),
    path('shoes/list/<kidsProfileId>', views.shoes_data_list, name='shoes_data_list'),
    path('shoes/data_add/', views.shoes_data_add, name='shoes_data_add'),
    path('shoes/data_edit/<shoesDataId>', views.shoes_data_edit, name='shoes_data_edit'),
    path('shoes/data_delete/<shoesDataId>', views.shoes_data_delete, name='shoes_data_delete'),
]
```

# View
最後にView。なんか、もっと上手く書けるような気がしてならないですが。。

（shoes_data_list）
・初期表示の際に、最初に登録された子供（IDが一番若い）の情報を出すように。

（shoes_data_add）
・画像ファイルはrequest.FILES.get()で受け取らないといけないらしい。
　アップロードが面倒な人もいるかもしれないので、Validation回避。

（shoes_data_edit）
・django-cleanupを動かすために、変更前のレコードはdelete()している。
　UpdateではなくCreate&Deleteすることで、紐付く画像ファイル自体を自動で削除。


```sheoes/views.py
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .models import ShoesData
from users.models import KidsProfile
from .forms import ShoesDataForm, ShoesDataEditForm


#靴リスト
@login_required
def shoes_data_list(request, **kwargs):
    user_name = request.user
    if len(kwargs) > 0:
        kidsProfileId = kwargs["kidsProfileId"]
    else:
        kidsProfileId = KidsProfile.objects.filter(user=user_name).order_by('id')[0].id

    kids_profiles = KidsProfile.objects.filter(user=user_name) #子供情報選択用
    kids_profile  = KidsProfile.objects.filter(id=kidsProfileId)
    shoes_data_posts = ShoesData.objects.filter(user=user_name, kidsProfile=kidsProfileId).order_by('buy_date')

    params = {
        'shoes_data_posts' : shoes_data_posts,
        'kidsProfiles' : kids_profiles,
        'kidsName' : kids_profile[0].name,
    }
    return render(request, 'shoes/shoes_data_list.html', params)

#靴データを新規追加
@login_required
def shoes_data_add(request):
    user_name = request.user
    if request.method == 'POST':
      form = ShoesDataForm(request.POST, user = user_name)
      if form.is_valid():
        data = form.save(commit=False)
        if request.FILES.get('shoes_image') is None:
            pass
        else:
            data.shoes_image = request.FILES.get('shoes_image')
        data.user = user_name
        data.save()
        return redirect('shoes:shoes_data_list')
    else:
      form = ShoesDataForm(user = user_name)
    return render(request, 'shoes/shoes_data_add.html', {'form': form})

#靴データを編集
@login_required
def shoes_data_edit(request, shoesDataId):
    shoes_data = ShoesData.objects.get(pk=shoesDataId)
    if request.method == 'POST':
      form = ShoesDataEditForm(request.POST)
      if form.is_valid():
        data = form.save(commit=False)
        data.user = request.user
        data.kidsProfile = shoes_data.kidsProfile
        data.shoes_image = request.FILES.get('shoes_image')
        data.save()
        shoes_data.delete()
        return redirect('shoes:shoes_data_list')
    else:
      form = ShoesDataEditForm(
        {
          'buy_date' : shoes_data.buy_date ,
          'shoes_size' : shoes_data.shoes_size ,
          'shoes_memo' : shoes_data.shoes_memo ,
          'shoes_image' : shoes_data.shoes_image
        },
        user = request.user
      )
    return render(request, 'shoes/shoes_data_edit.html', {'form': form})

#靴データを削除
@login_required
def shoes_data_delete(request, shoesDataId):
    ShoesData.objects.get(id=shoesDataId).delete()
    return redirect('shoes:shoes_data_list')
```

# 動かすよ〜
画像をアップしなかったものと、アップしたもの。
<img width="1440" alt="スクリーンショット 2020-02-13 00.20.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547363/738fe7da-8062-19cc-818e-081f76923c02.png">
登録画面はシンプル。
<img width="1440" alt="スクリーンショット 2020-02-13 00.21.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547363/7875ea44-8471-7c76-7b04-bb550b9d8601.png">