前置き
独学で、子供の成長アプリを作った時のことを、記録として残していきます。
間違っているところなどあれば、ご連絡お願いします。
①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の画像はこちらで用意。
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も作ります。
なんだか冗長な気がしましたが、これ以上のやり方もわからず。
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フォルダに格納しておいてください。
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にも、いろいろ追加しましょう。
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)
ひねりなし。言うこともなし。
{% 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 %}
{% 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>
{% 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 %}
{% 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 %}
.shoes_img {
background-size: cover;
background-position: center center;
}
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することで、紐付く画像ファイル自体を自動で削除。
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')