1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-12

前置き

独学で、子供の成長アプリを作った時のことを、記録として残していきます。
間違っているところなどあれば、ご連絡お願いします。
 ①Djangoのようこそページへたどり着くまで
 ②NginxでDjangoのようこそページへたどり着くまで
 ③カスタムユーザーを作ってadminにたどり着く
 ④ログインログアウトをしよう
 ⑤ユーザー登録(サインイン)機能を作ろう
 ⑥ユーザーごとのデータ登録できるようにする〜CRU編
 ⑦ユーザーごとのデータ登録できるようにする〜削除編
 ⑧画像ファイルのアップロード <--ここです
 ⑨身長体重を記録する@一括削除機能つき
 ⑩成長曲線グラフを描いてみよう
 ⑪本番環境へデプロイ+色々手直し

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)

画像を扱おう(準備)

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

・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="">メニュー1</a>
                  </li>
                  <li class="nav-item">
                      <a class="nav-link" href="">メニュー2</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')

動かすよ〜

画像をアップしなかったものと、アップしたもの。
スクリーンショット 2020-02-13 00.20.38.png
登録画面はシンプル。
スクリーンショット 2020-02-13 00.21.00.png

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?