#はじめに
ん~、最近Webの勉強が楽しい!
さくらのVPS借りてhelloDjangoするまでにサーバーまわりで挫折を繰り返して1年半近く費やしたからな。。。画面が映ることが保証されてる状態の楽しさったらないわ。ルンルンですわ
Qiitaもはかどる!どんどんいこう!
#今回作る機能
CSVをブラウザからアップロードして、MySQLに登録。即座にグラフに反映される
#読める!読めるぞ!!
むむっ!?気がついたら公式ドキュメントが読めるようになってきてるじゃないか!?完全に導入時点の壁超えた感じある
ここに来るまでに何度挫折を味わったか...
Django公式:ファイルのアップロード
bootstrap4公式:フォーム
Djangoでファイルアップローダを作る
#フォームの作成
##おさらい(フォームとは?)
ほれ、ユーザーからデータがきたぞ~。っていうこれのことです!(アンケート入力して送信~って仕組み)
$ vi pj1/app1/forms.py
from django import forms
class UploadFileForm(forms.Form):
# formのname 属性が 'file' になる
file = forms.FileField(required=True, label='')
$ vi pj1/app1/views.py
from django.shortcuts import render, redirect
from .models import evaluation
from .forms import UploadFileForm
import os
UPLOAD_DIR = os.path.dirname(os.path.abspath(__file__)) + '/static/files/'
def index(request):
if request.method == 'POST':
# POST
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
# file is saved. by binary
f = request.FILES['file']
path = os.path.join(UPLOAD_DIR, f.name)
with open(path, 'wb') as destination:
for chunk in f.chunks():
destination.write(chunk)
return redirect('index')
else:
# GET
form = UploadFileForm()
qry = evaluation.objects.order_by('-ym')[:1].values('ym','homogeneity','plowing','biological','chemical','hardness')
return render(request, 'index.html', {'rs': qry, 'cs':qry[0].keys(), 'form':form})
$ vi pj1/app1/templates/index.html
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<div class="form-group">
<button type="submit">UPLOAD</button>
</div>
</form>
##アップロード先のディレクトリをつくる
$ mkdir pj1/app1/static/files
##日本語対応
ファイル名に日本語(全角文字)が含まれているファイルをアップロードするための設定
$ sudo vi /etc/sysconfig/httpd
LANG='ja_JP.UTF-8'
$ systemctl restart httpd
##UPLOADテスト
2.ファイルが選択されていない状態でアップロードボタン押すとはじかれる
3.ファイル見てみようぜ
$ vi pj1/app1/static/files/sample.csv
##UPLOAD先からcsvを読んでDB登録
PythonスクリプトでDjangoにデータを登録する
$ vi pj1/app1/views.py
from django.shortcuts import render, redirect
from .models import evaluation
from .forms import UploadFileForm
import os, csv
UPLOAD_DIR = os.path.dirname(os.path.abspath(__file__)) + '/static/files/'
def index(request):
if request.method == 'POST':
# POST
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
# file is saved. by binary
f = request.FILES['file']
path = os.path.join(UPLOAD_DIR, f.name)
with open(path, 'wb') as destination:
for chunk in f.chunks():
destination.write(chunk)
# open csv
with open(path, 'r') as destination:
# read csv
rdr = csv.reader(destination)
# ignore header
next(rdr)
# upsert
for r in rdr:
ev = evaluation()
ev.ym = r[0]
ev.homogeneity = float(r[1])
ev.plowing = float(r[2])
ev.biological = float(r[3])
ev.chemical = float(r[4])
ev.hardness = float(r[5])
ev.save()
return redirect('index')
else:
# GET
form = UploadFileForm()
qry = evaluation.objects.order_by('-ym')[:1].values('ym','homogeneity','plowing','biological','chemical','hardness')
return render(request, 'index.html', {'rs': qry, 'cs':qry[0].keys(), 'form':form})
$ systemctl restart httpd
##登録できた!
おおぉ~!やったやんけ~!
##グラフも即座に変わった!
「systemctl restart httpd」やらなくても平気になったな!
#バリデーション強化
ここでまたバリデーションについて公式ドキュメントと格闘することに。なんで英語なんだよー
公式:フォームとフィールドの検証
Django、CSVのインポート・エクスポート ~バリデーション~
##入力チェックの動き
■Django フォームのバリデーション (入力検証)
Django のフォームシステムでは、デフォルトの入力検証が終わった後に、clean_ から始まり、 フィールド名のついたメソッドを呼出します。この clean_* メソッドによって、カスタムの入力検証を行うことができます。 例えば、email というフィールドに対しては、clean_email というメソッド名で入力検証を行います。
###待てよ?
タッカーさん。もひとつ質問いいかな。is_valid()がしくじったときって何の画面が返ってるんだ?
実際の動き的には、アップロードボタンを押したときに(画面遷移なしで)ここがダメだよって怒られるんだから、入力チェックが終わった後の動きが尻切れトンボで、結果的に動き(画面遷移)なし、ってことか?まぁ納得はできる動きか?
いや?やっぱり変だぞ?
とりあえず、ダメなパターンでアップロードしようとして、、、っと
ほらきた!
予想通りのエラーが起きたのはある意味では気持ちいいけど...w
公式ドキュメントはこの辺は考慮されていないんだな。
###returnの位置を移動する
わかります?一番下の2行のインデントが左に寄りました。これによって、POSTにいったあとの最後で index に redirect してたやつが省力化できた。
from django.shortcuts import render, redirect
from .models import evaluation
from .forms import UploadFileForm
import os, csv
UPLOAD_DIR = os.path.dirname(os.path.abspath(__file__)) + '/static/files/'
def index(request):
if request.method == 'POST':
# POST
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
# file is saved. by binary
f = request.FILES['file']
path = os.path.join(UPLOAD_DIR, f.name)
with open(path, 'wb') as destination:
for chunk in f.chunks():
destination.write(chunk)
# open csv
with open(path, 'r') as destination:
# read csv
rdr = csv.reader(destination)
# ignore header
next(rdr)
# upsert
for r in rdr:
ev = evaluation()
ev.ym = r[0]
ev.homogeneity = float(r[1])
ev.plowing = float(r[2])
ev.biological = float(r[3])
ev.chemical = float(r[4])
ev.hardness = float(r[5])
ev.save()
else:
# GET
form = UploadFileForm()
qry = evaluation.objects.order_by('-ym')[:1].values('ym','homogeneity','plowing','biological','chemical','hardness')
return render(request, 'index.html', {'rs': qry, 'cs':qry[0].keys(), 'form':form})
##CSVのみ許容する
xxxというフォームフィールドを作ったら「is_valid()」が走ったときに clean_xxx がトリガされるのね。で、入力チェックが終わると self.cleaned_data というプロパティに、入力チェックが完了した入力データがコピーされるっぽい
$ vi pj1/app1/forms.py
from django import forms
class UploadFileForm(forms.Form):
# formのname 属性が 'file' になる
file = forms.FileField(required=True, label='')
# add
def clean_file(self):
file = self.cleaned_data['file']
if file.name.endswith('.csv'):
return file
else:
raise forms.ValidationError('拡張子はcsvのみです')
$ systemctl restart httpd
###CSV以外をハジいた!
「form.is_valid()でエラーが発生すると form の中にエラー情報を抱え込むので、そのまま form を render に投げて template に渡ればエラーがでたあとの index.html に遷移する!」これだ。メッセージの出かたがダサいのはCSSかなんかで吹き出しにできるのかな?
###エラーメッセージにCSS
form がエラーを抱えて render されると、errorlist としてクラスがセットされて書き出される。
<ul class="errorlist">
<li>拡張子がcsvのファイルをアップロードしてください</li>
</ul>
で、スタイルが勝手に適用されると思ってたんだけど、、、
Adding css class to field on validation error in django
・html li リストマーク(・)消す方法
・おしゃれなボックスデザイン(囲み枠)のサンプル30
$ mkdir pj1/app1/static/css
$ vi pj1/app1/static/css/styles.css
.errorlist {
padding: 0.5em 1em;
margin: 2em 0;
color: #ff7d6e;
background: #ffebe9;
border-top: solid 10px #ff7d6e;
}
.errorlist li {
margin: 0;
padding: 0;
list-style: none;
}
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}" />
$ systemctl restart httpd
##項目数が正しいか
バリデーション
#項目数バリデーションから発展して結構変える羽目になった
苦しめられてるのは、リストとか辞書でテンプレートに投げたデータの、テンプレート側での開き方だよなぁ~
あと、当たり前っちゃ当たり前だけど「CSVのバリデーションは、フォームのバリデーションじゃない」ってことに気がついた。
if len(r) == len(HEADER):
・
・
・
else:
raise form.ValidationError('{0}行目、項目数エラー'.format(i))
・nレコードを表示する前提だったけど .first で1レコードだけ返すことにした
スライス「:1」で1レコードだけに切ったときと「.first()」で完全に1レコードしか渡さなかったときとはテンプレート側での動きが違うようだ。ヤヤコシス。
・上記の結果、テンプレート側でループ構文にアテなくても良くなった(シンプルになった)
・0レコードだったとき、に対応した
index.html ~ヘッダー部~
・CSSを使うよ!という宣言と、CSSへのパスを追加
index.html ~データテーブル部~
・「.first()」を使うことでループ構文にアテなくても良くなった
index.html ~javascript グラフデータ代入部~
・「.first()」を使うことでループ構文にアテなくても良くなった
view.py ~冒頭~
・「evaluation(=評価)」というテーブルには隠れ項目「id」列があるため、それ以外の項目を列挙した。POSTルート時の「CSVとテーブルの項目数の整合性点検」のときにも使われている
view.py ~db登録部とクエリ部とリターン部~
・CSV項目数とテーブルの規定項目数が違うときという診断を入れた
・add_error で form のエラーに成りすませることを発見!
・0レコードだったとき、に対応
・nレコードを表示する前提だったけど .first で1レコードだけ返すことにした
/. ノ、i.|i 、、 ヽ
i | ミ.\ヾヽ、___ヾヽヾ |
| i 、ヽ_ヽ、_i , / `__,;―'彡-i |
i ,'i/ `,ニ=ミ`-、ヾ三''―-―' / .|
iイ | |' ;'(( ,;/ '~ ゛  ̄`;)" c ミ i.
.i i.| ' ,|| i| ._ _-i ||:i | r-、 ヽ、 / / / | _|_ ― // ̄7l l _|_
丿 `| (( _゛_i__`' (( ; ノ// i |ヽi. _/| _/| / | | ― / \/ | ―――
/ i || i` - -、` i ノノ 'i /ヽ | ヽ | | / | 丿 _/ / 丿
'ノ .. i )) '--、_`7 (( , 'i ノノ ヽ
ノ Y `-- " )) ノ ""i ヽ
ノヽ、 ノノ _/ i \
/ヽ ヽヽ、___,;//--'";;" ,/ヽ、 ヾヽ
#ソースの整理
from django.shortcuts import render, redirect
from .models import evaluation
from .forms import UploadFileForm
import os, csv
UPLOAD_DIR = os.path.dirname(os.path.abspath(__file__)) + '/static/files/'
def index(request):
HEADER = ['ym','homogeneity','plowing','biological','chemical','hardness']
if request.method == 'POST':
# POST
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
# file is saved. by binary
f = request.FILES['file']
path = os.path.join(UPLOAD_DIR, f.name)
with open(path, 'wb') as destination:
for chunk in f.chunks():
destination.write(chunk)
# open csv
with open(path, 'r') as destination:
# read csv
rdr = csv.reader(destination)
# csv records count (first record is header)
cnt = sum(1 for r in destination)
destination.seek(0)
# When there is a records.
if cnt >0:
# ignore header
next(rdr)
# upsert
for i, r in enumerate(rdr, 1):
if len(r) == len(HEADER):
ev = evaluation()
ev.ym = r[0]
ev.homogeneity = float(r[1])
ev.plowing = float(r[2])
ev.biological = float(r[3])
ev.chemical = float(r[4])
ev.hardness = float(r[5])
ev.save()
else:
form.add_error(None,'{0}行目 項目数エラー(規定項目数:{1}, CSV項目数:{2})'.format(i, len(HEADER), len(r)))
else:
# GET
form = UploadFileForm()
# Assemble and Exec SQL
qry = evaluation.objects.all()
if qry.count() >0:
qry = evaluation.objects.order_by('-ym').first()
# Rendering
return render(request, 'index.html', {'rs': qry, 'cs':HEADER, 'form':form})
<!DOCTYPE html>
{% load staticfiles %}
{% load static %}
<html lang="ja">
<head>
<!-- Stylesheet and meta necessary for Bootstrap -->
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}" />
<!-- Google fonts -->
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,300' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'>
<!-- D3.js -->
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script src="https://d3js.org/d3-path.v1.min.js" charset="utf-8"></script>
<script src="{% static 'js/radarChart.js' %}" charset="utf-8"></script>
<style>
.legend {
font-family: 'Raleway', sans-serif;
fill: #333333;
}
</style>
<title>Anaconda!</title>
</head>
<body>
<h1>Hello, Bootstrap world!</h1>
<table class="table table-dark table-hover">
<thead>
<tr>
{% for c in cs %}
<td>{{ c }}</td>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
<td>{{ rs.ym }}</td>
<td>{{ rs.homogeneity }}</td>
<td>{{ rs.plowing }}</td>
<td>{{ rs.biological }}</td>
<td>{{ rs.chemical }}</td>
<td>{{ rs.hardness }}</td>
</tr>
</tbody>
</table>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<div class="form-group">
<button type="submit">UPLOAD</button>
</div>
</form>
<div class="radarChart" style="display: inline-flex;"></div>
<script>
//////////////////////////////////////////////////////////////
//////////////////////// Set-Up //////////////////////////////
//////////////////////////////////////////////////////////////
var margin = { top: 50, right: 80, bottom: 50, left: 80 },
width = Math.min(700, window.innerWidth / 4) - margin.left - margin.right,
height = Math.min(width, window.innerHeight - margin.top - margin.bottom);
//////////////////////////////////////////////////////////////
////////////////////////// Data //////////////////////////////
//////////////////////////////////////////////////////////////
var data = [
{ name: 'Mean',
axes: [
{axis: 'homogeneity', value: 40},
{axis: 'plowing', value: 40},
{axis: 'biological', value: 40},
{axis: 'chemical', value: 40},
{axis: 'hardness', value: 40}
]
},
{ name: 'You',
axes: [
{axis: 'homogeneity', value: 0},
{axis: 'plowing', value: 0},
{axis: 'biological', value: 0},
{axis: 'chemical', value: 0},
{axis: 'hardness', value: 0}
]
}
];
// views.py からの代入
data[1].axes[0].value = parseFloat("{{ rs.homogeneity }}");
data[1].axes[1].value = parseFloat("{{ rs.plowing }}");
data[1].axes[2].value = parseFloat("{{ rs.biological }}");
data[1].axes[3].value = parseFloat("{{ rs.chemical }}");
data[1].axes[4].value = parseFloat("{{ rs.hardness }}");
var radarChartOptions = {
w: 290,
h: 350,
margin: margin,
maxValue: 60,
levels: 6,
roundStrokes: false,
color: d3.scaleOrdinal().range(["#AFC52F", "#ff6600"]),
format: '.0f',
legend: { title: 'TOTAL EVALUATION', translateX: 100, translateY: 40 }
};
RadarChart(".radarChart", data, radarChartOptions);
</script>
<!-- jQuery necessary for Bootstrap -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js" integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm" crossorigin="anonymous"></script>
</body>
</html>