naritoブログ

【お知らせ】
新ブログができました。今後そちらで更新し、このサイトは更新されません(ウェブサイト自体は残しておきます)
このブログの内容に関してコメントしたい場合は、新ブログのフリースペースに書き込んでください

このブログの内容を新ブログに移行中です。このブログで見つからない記事は、新ブログにありま

Djangoで、進捗表示する

約318日前 2017年12月30日20:57
プログラミング関連
Django Python
今回は、以下のような進捗表示するページを作成します。


例えばボタンを押すとDjango側で重たい処理が開始され、レスポンスが帰るまで時間がかかる...その間に進捗を表示させたい、そんな場合に使えます。
このような進捗の表示にはWebSocketを使うことも考えられますが、今回は素のDjangoで作ります。
パフォーマンス的には少々問題があるのですが、ローカル環境で自分だけで使う分には問題ないかもしれません。
Nginx + Gunicornにて実際のサーバーで試しても動作はしました。


まず、以下のようなモデルを定義します。
進捗となる数値フィールドだけを持つ、シンプルなモデルです。

from django.db import models


class Progress(models.Model):
"""進捗を表すモデル"""
num = models.IntegerField('進捗', default=0)

def __str__(self):
return self.num




urls.pyにて、2つのビューを定義します。トップページと、進捗表示ページです。

from django.urls import path
from . import views

app_name = 'app'

urlpatterns = [
path('', views.HomeView.as_view(), name='index'), # トップページ
path('progress/<int:pk>/', views.progress, name='progress'), # 進捗が表示されていくページ
]




次にviews.pyです。後で説明します。

from multiprocessing import Process
import time

from django.shortcuts import redirect, render, get_object_or_404
from django.views import generic
from .models import Progress


def update(pk):
"""裏側で動いている時間のかかる処理"""
progress = get_object_or_404(Progress, pk=pk)
for i in range(1, 11):
time.sleep(1)
progress.num = i * 10 # 初回に10、次に20...最後は100が入る。進捗のパーセントに対応
progress.save()


class HomeView(generic.CreateView):
"""処理の開始ページ"""
model = Progress
fields = ()
template_name = 'app/home.html'

def form_valid(self, form):
progress_instance = form.save()
p = Process(target=update, args=(progress_instance.pk,), daemon=True)
p.start()
return redirect('app:progress', pk=progress_instance.pk)


def progress(request, pk):
"""現在の進捗ページ"""
context = {
'progress': get_object_or_404(Progress, pk=pk)
}
return render(request, 'app/progress.html', context)




共通のテンプレートとなるbase.html
Bootstrap4の雛形を基に、{% block content %}を置いて継承先で上書きができるように準備。
見た目のためにcontainerで左右余白を作り、mt-5で、画面上端にくっつかないように。

<!doctype html>
<html lang="ja">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy" crossorigin="anonymous">

<title>Hello, world!</title>
</head>
<body>
<div class="container mt-5">
{% block content %}
{% endblock %}
</div>

<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/js/bootstrap.min.js" integrity="sha384-a5N7Y/aK3qNeh15eJKGWxsqtnX/wWdSZSKp+81YjTmS15nvnvxKHuzaWwXHDli+4" crossorigin="anonymous"></script>
</body>
</html>





トップページ、処理開始ボタンがあるhome.html
Progressモデルのフィールドは一つで、その上デフォルト値を持ちます。中に何か値を入れる必要もなく、デフォルト値の0のままで大丈夫です。
なのでHomeViewのfields は空タプルにし、このテンプレートでも {{ form }}は使っていません。それでもなお、form.save()は問題なく動きます。
モデルのフィールドが全てデフォルト値や空欄を許すような場合は、今回のようなこともできます。

{% extends 'app/base.html' %}

{% block content %}
<form action="" method="POST">
{% csrf_token %}
<button type="submit" class="btn btn-primary">処理の開始</button>
</form>
{% endblock %}





進捗表示ページ、progress.html

{% extends 'app/base.html' %}

{% block content %}
<meta http-equiv="refresh" content="3">
{% if progress.num >= 100 %}
処理終了
{% else %}
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="{{ progress.num }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ progress.num }}%"></div>
</div>
{% endif %}
{% endblock %}




このmetaタグによって、3秒おきにページが更新されます。

<meta http-equiv="refresh" content="3">



このテンプレートにはprogressモデルインスタンスが渡されます。
numが100以上になっていたら処理が終了したと判断し、そうでなければBootstrap4の進捗バーを使います。進捗部分は、{{ progress.num }}としています。
Gifで見ると何かうねってみえますが、もともとそういう見た目のウィジェットです。便利ですね。

{% if progress.num >= 100 %}
処理終了
{% else %}
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="{{ progress.num }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ progress.num }}%"></div>
</div>
{% endif %}



今回の大まかな処理の流れは
1. home.htmlの「処理の開始ボタン」が押される
2. HomeViewにて、Progressモデルインスタンスを作成(numフィールドは0)

3. 先程のインスタンスのpkを別プロセスで実行するupdate関数に渡し、numフィールドの値を増やし続ける
4. progressビューにリダイレクトさせて、先程のインスタンスを取得してprogress.htmlを表示する

Progressモデルインスタンスのnumフィールドが100を超えるまで3の処理が続き、
progress.htmlの<meta http-equiv="refresh" content="3">によってprogressビューは3秒おきにアクセスされます。
numフィールドは毎秒値が更新されており、その値を現在の進捗として表示する、という流れです。

今回、進捗を表示するのにページの自動リロードを行うアプローチにしました。<meta http-equiv="refresh" content="3">ですね。
これをAjaxを使うようにすれば、もう少しスムーズになりそうです。


views.pyの処理も説明します。
update関数は、別プロセスで行うnumフィールドの増減を行うための処理です。
1秒ごとに10増やして保存します。進捗を表すnumフィールドを増やしたいだけなので、この辺は自由に書けます。

def update(pk):
"""裏側で動いている時間のかかる処理"""
progress = get_object_or_404(Progress, pk=pk)
for i in range(1, 11):
time.sleep(1)
progress.num = i * 10 # 初回に10、次に20...最後は100が入る。進捗のパーセントに対応
progress.save()



form_valid内で保存し、新しいProgressモデルインスタンスを取得します。
pkを別プロセスのupdate関数に渡し、進捗を進めてもらう準備ができたら、リダイレクトで進捗表示ページに移動します。

class HomeView(generic.CreateView):
"""処理の開始ページ"""
model = Progress
fields = ()
template_name = 'app/home.html'

def form_valid(self, form):
progress_instance = form.save()
p = Process(target=update, args=(progress_instance.pk,), daemon=True)
p.start()
return redirect('app:progress', pk=progress_instance.pk)



進捗表示ページのビューは、単純にProgressモデルインスタンスの取得と、それをテンプレートへ渡すだけです。

def progress(request, pk):
"""現在の進捗ページ"""
context = {
'progress': get_object_or_404(Progress, pk=pk)
}
return render(request, 'app/progress.html', context)


Mixinなんかを使うと、余計なコードを分離できるかもしれませんね。