naritoブログ

このブログはPython/DjangoとBootstrap4で作成されました
Githubのソースコード

Djangoで、進捗表示する

約148日前 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なんかを使うと、余計なコードを分離できるかもしれませんね。