DjangoでWEBアプリを作成する 3

###はじめに
　２回目の投稿では、DjangoのModelを使ってデータベースの操作を簡単に解説しました。しかしそれはModelとFormの連携のなかで行われた登録（更新）削除でした。今回の投稿ではデータの抽出やグラフを使ったデータ表現（分析）をしたいと思います。
引き続き、前回の投稿(<https://qiita.com/cloghjordan/items/08caf9955d05e7bd127d>)で記述したファイルを使用します。

###1.受注明細の登録
　それでは早速受注明細の登録ができるよう、これまで同様のステップで行きたいと思いまが、今回はForm.pyもそこに加えます。
①urls.py

~~~
urlpatterns = [
    path('', views.index, name='index'),
    path('item_list', views.item_list, name='item_list'),
    path('add_item', views.add_item, name='add_item'), 
    path('dtl_item', views.dtl_item, name='dtl_item'),
    path('add_order', views.add_order, name='add_order'), #<- 追記します 
]
~~~
②From.py
　品目マスタ登録（更新用）にフォームを作成しましたが、そのときはModelFormから派生させたクラスでした。受注明細の登録フォームはFormから派生させてみせます。それは、入力項目＝モデルのフィールドとしなかったことにも原因しています。
まずは、Orderモデルと幾つか必要なモジュールをインポートしておきます。
結果的にFormはViewと密接度が強くなってしまったようなので、View.pyと見比べながら処理の連携を見ていただければと思います。

~~~
#-*- coding: utf-8 -*-
from django import forms
from .models import Item, Order  #<- Orderを追加する
from django.core.exceptions import ObjectDoesNotExist #<- 追加する
from datetime import datetime #<- 追加する
~~~
つづいて受注明細登録用のクラスです。
なるべく画面周りのことに関してはForm.pyのクラスの中で完結させてView.pyでは、必要な情報のみ取り扱うようにしたかったのですが、あまり良い出来では無いかもしれません。

~~~
#------------------------------------------------
# 受注明細登録用のフォーム
#------------------------------------------------
class OrderForm(forms.Form):

    date = forms.DateField(
        label= '日付',
        initial=datetime.now,
        required= True, # 必須
    )
    item_cd = forms.CharField(
        label='品目CD',
        required= True, # 必須
    )
    item_mei = forms.CharField(
        label='品目名',
        required= False, # 必須不要
    )
    quantity = forms.IntegerField(
        label='数量',
        required= True, # 必須
    )
    text = forms.CharField(
        label='摘要',
        widget=forms.Textarea,
        required= True, # 必須に変更
    )

    #--------------------------------------
    # init
    #--------------------------------------
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['item_mei'].widget.attrs = {'placeholder': '品目名を変更する場合は入力する'}
        self.fields['item_mei'].widget.attrs['size'] = 40
    
    #--------------------------------------
    # バリデーションチェックをする
    #--------------------------------------
    def clean_item_cd(self):
        try:
            item_cd = self.cleaned_data['item_cd']
            Item.objects.get(品目CD=item_cd)
            return item_cd
        except ObjectDoesNotExist:
            raise forms.ValidationError("入力された品目CDは品目マスタに登録されていません1。")

    #--------------------------------------
    # バリデーションチェックをする
    #--------------------------------------
    def clean(self):
        cleaned_data = super(OrderForm, self).clean()
        
        #品目に存在していない場合は、cleaned_dataにフィールが含まれていないことを利用
        if("item_cd" in cleaned_data):
            try:
                self.item= Item.objects.get(品目CD=cleaned_data['item_cd'])
                if(self.item):
                    #品目名は上書きも可とする
                    item_mei= cleaned_data['item_mei']
                    if(item_mei):
                        txt= item_mei
                    else:
                        txt= self.item.品目名
                    self.cleaned_data['item_mei']= txt 
                    
                    #価格はマスタより取得する
                    self.price= self.item.価格 
            except ObjectDoesNotExist:
                raise forms.ValidationError("入力された品目CDは品目マスタに登録されていません2。")

    #--------------------------------------
    # 金額を返す
    #--------------------------------------
    def get_amount(self):
        quantity= self.cleaned_data['quantity']
        amount= self.price * quantity
        return amount

    #--------------------------------------
    # Itemを返す
    #--------------------------------------
    def get_item(self):
        item_cd= self.cleaned_data['item_cd']
        item= Item.objects.get(品目CD= item_cd)
        return item
~~~
データの有効性チェックは順番があるようです。
入力項目ごと「clean_フィールド名()」のチェックが先で全体チェック「clean()」はその後みたいです。そして、入力項目ごとのチェックでエラーなった場合は、clean_dataにはフィールドの値はセットされない様です。
ここでは、View.pyから呼び出す関数も用意しています。関数を使って受注明細の登録に必要となる外部キー（Item）と金額を提供しています。

③views.py
　今回はCreateFormからではなくFromViewから派生させてみます。View.pyに下記を追加して下さい。

~~~
from .forms import HinmokuForm, OrderForm  #<- OrderFormを追加

#-------------------------------------------------------------
# 新規受注明細登録
#-------------------------------------------------------------
class AddOrderView(FormView):
    form_class= OrderForm
    template_name= 'items/order_form.html'
    success_url= '/items/'

    def form_valid(self, form):
        #オブジェクトをフォームから取得
        date= form.cleaned_data['date']
        item= form.get_item()
        price= form.item.価格
        item_mei= form.cleaned_data['item_mei']
        quantity= form.cleaned_data['quantity']
        amount= form.get_amount()
        text= form.cleaned_data['text']

        Order.objects.create(日付= date, 品目CD= item, 品目名= item_mei, 価格= price, 数量= quantity, 金額= amount, 摘要= text)

        return super().form_valid(form)

add_order= AddOrderView.as_view()
~~~
Viewで受注明細を登録（Order.objects.create）しますが、clean_dataの値や、formの関数などから値を得ているところは汚い書き方ですがご勘弁下さい。

④HTML
④-1:受注明細登録画面作成(order_form.html)

~~~
<div>
    <form method="POST">
        {% csrf_token %}
        <table>
            {{ form }}
        </table>
        <br>
        <input type="submit" name="button_1" value="保存する">
    </form>
</div>
<br>
~~~

④-2:一覧画面修正・・・「受注明細の登録」の箇所追加

~~~
<html>
	<head>
	</head>
	<body>
		<div>ビート店員のあいさつ：
			{% for bb in opinion %}
				<p>{{ bb }}</p>
			{% endfor %}
		</div>
		<h3>
			<a href="{% url 'items:item_list' %}">品目の一覧表示</a>
		</h3>
		<h3><a href="{% url 'items:add_order' %}">受注明細の登録</a></h3>
	</body>
</html>
~~~
Djangoを使った開発では、或いはFormを使うことは必須では無いような気がします。恐らくWEB開発になれた方であれば、画面周りはhtmlとViewの中でのみ対応した方が早く済んでしまうからかもしれません。しかし折角提供されている機能ですから、Formに拘ってみたいと思いました。

プログラムが書き変えられたところで、動作を確認してみて下さい。
受注明細登録の画面を掲載します。
![キャプチャ.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440325/401e1af5-c459-1a10-cdb6-b55fb021067f.jpeg)

![キャプチャ60.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440325/84878678-ac1e-3dd8-062a-98f6ab3fa5b7.jpeg)
無事に受注明細の登録が出来ていれば良いのですが。。。

###2.shellからのコマンド操作を再び
　受注明細を大量に作りたいので、これからshellのコマンド操作でその作業を行いたいと思います。結論から云うと、下記の様な書き方によるデータの登録・更新・削除はとても時間がかかります。したがって、データ量が多い場合にはbulk_create（登録）・bulk_update（更新）~~・bulk_delete（削除）~~を使うことをおすすめします。このあたりは、下記のリンク先が参考になると思います。

Djangoで、データの一括作成・一括更新(<https://narito.ninja/blog/detail/132/>)

簡易WEBサーバーが起動している場合は停止して、shellを起動して下さい。

~~~
mysite> python manage.py shell
~~~

~~~
Python 3.7.4 (default, Aug  9 2019, 18:34:13) [MSC v.1915 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.10.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import random

In [2]: from items.models import Item, Order

In [3]: items = Item.objects.all()

In [4]: item_list= list(items.values_list('品目CD', flat=True))

In [5]: for i in range(1000):
   ...:     x= random.choice(item_list)
   ...:     y= Item.objects.get(品目CD=x)
   ...:     new = Order(日付="2019-12-24", 品目CD=y, 品目名= y.品目名, 価格= y.価格, 数量= 10, 金額= 10*y.価格)
   ...:     new.save()
   ...:

In [6]: Order.objects.all().count()
Out[6]: 1002

In [7]:
~~~
1000件程度のデータ登録ですが、私の環境でも１分程度かかります。しかし、一件づつのsaveを行わずにbulk_createでデータの一括登録を実行すると劇的に早くなるのでおもしろい。

###3.登録された受注データを分析する
受注明細がある程度出来たところで、データ操作の醍醐味を味わいましょう。
ここでは、どれほど受注できているのかを品目別に調べてみたいと思います。

それでは、いつもの手順で画面を作っていきましょう。
①urls.py

~~~
urlpatterns = [
    path('', views.index, name='index'),
    path('item_list', views.item_list, name='item_list'),
    path('add_item', views.add_item, name='add_item'), 
    path('dtl_item/<int:pk>', views.dtl_item, name='dtl_item'),
    path('upd_item/<int:pk>', views.upd_item, name='upd_item'), 
    path('del_item/<int:pk>', views.del_item, name='del_item'),  
    path('add_order', views.add_order, name='add_order'),
    path('order_dsp', views.order_dsp, name='order_dsp'), #<- 追記します 
]
~~~

②views.py
まずデータの集計を行いますので、集計用のモジュールをインポートします。

~~~
from django.db.models import Count, Sum #<- 追加した
~~~
続いて画面表示用のクラスを作りますが、今回はTemplateViewから継承させました。

~~~
#---------------------------------------------------------------
#受注明細の登録状況確認
#---------------------------------------------------------------
class OrderDspView(TemplateView):
    template_name = "items/order_dsp.html"
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        data= Order.objects.values_list('品目CD', '品目名').annotate(gp_qut=Sum('数量'), gp_amt=Sum('金額')).order_by('-gp_amt')
        context["data"] = data

        cat=[]
        amt=[]
        qut=[]
        for itm in data:
            cat.append(itm[1]) #２つ目の要素は「品目名」
            qut.append(itm[2]) #３つ目の要素は「数量」
            amt.append(itm[3]) #４つ目の要素は「金額」

        context['cat']= cat
        context['amt']= amt
        context['qut']= qut
        
        return context
order_dsp = OrderDspView.as_view()
~~~
データ操作という意味ではこの一文でデータ抽出しています。

~~~
data= Order.objects.values_list('品目CD', '品目名').annotate(gp_qut=Sum('数量'), gp_amt=Sum('金額')).order_by('-gp_amt')
~~~
ここではグループ集計を行っています。values_listで表示フィールドを選択したいます。annotate部分で数量と金額について合計(Sum)して項目名を与えています。order_byで金額による並び替えを指示していますが、「-gp_amt」のマイナス記号は「降順」を意味します。
抽出されたデータ（ここでは data）はquerysetという型でDjangoオリジナルの型らしいです。
残りの部分では、querysetデータをhtmlに渡すためにリストに変換してcontextにセットしています。


③HTML
④-1:一覧画面修正・・・「受注明細の登録状況」の箇所追加

~~~
<html>
	<head>
	</head>
	<body>
		<div>ビート店員のあいさつ：
			{% for bb in opinion %}
				<p>{{ bb }}</p>
			{% endfor %}
		</div>
		<h3>
			<a href="{% url 'items:item_list' %}">品目の一覧表示</a>
		</h3>
		<h3><a href="{% url 'items:add_order' %}">受注明細の登録</a></h3>
		<h3><a href="{% url 'items:order_dsp' %}">受注明細の登録状況</a></h3>
	</body>
</html>
~~~
③-2:受注明細登録画面作成(order_dsp_form.html)
テンプレートは、【items】フォルダ-【templates】フォルダ-【items】フォルダの中に作成して下さい。
そして折角ですから、データ集計結果はテーブルとグラフ(Highcharts)で表示したいと思います。

~~~
{% load static %}

<br>
<table border="0">
    <h4>受注明細の集計</h4>
    <tr>
        <td id="container" style="width: 700px; height: 400px;"></td>
    </tr>
    <tr>
    </tr>
    <tr>
        <table border="2" width="700" cellspacing="0" bordercolor="black">
            <caption>商品の一覧</caption>
            <tr bgcolor="lightgray">
                <th>品目CD</th>
                <th>品目名</th>
                <th>数量</th>
                <th>金額</th>
            </tr>
            
            {% for dt in data %}
            <tr>
                <td align="left">{{dt.0}}</td>
                <td align="left">{{dt.1}}</td>
                <td align="right">{{dt.2}}</td>
                <td align="right">{{dt.3}}</td>
            </tr>
            {% endfor %}
        </table>
    </tr>
</table>

<!--HightChartで必要な部品----------------------------------------------------------------------->
<script src="{% static '/highcharts/code/highcharts.src.js' %}"></script> <!--必須-->
<script src="{% static '/highcharts/code/modules/exporting.js' %}"></script> <!--右上メニュー：イメージ出力で必要-->
<script src="{% static '/highcharts/code/modules/export-data.js' %}"></script> <!--右上メニュー：データ出力で必要-->
<!--------------------------------------------------------------------------------------------->

<!--表示するグラフの設定------------------------------------------------------------------------>
<script>
    Highcharts.chart('container', {
        chart: {
            type: 'column'
        },
        title: {
            text: '品目別の受注額'
        },
        subtitle: {
            text: '(金額単位：円)'
        },
        xAxis: {
            categories: [
                {% for c in cat %}
                    '{{ c }}' {% if not forloop.last %} , {% endif %}
                {% endfor %}
            ],
            labels: {
                rotation: -45,
                style: {
                    fontSize: '13px',
                    fontFamily: 'Verdana, sans-serif'
                }
            }
        },
        yAxis: [{
            min: 0,
                title: {
                    text: '受注額 (円)'
                },
            },
            {// 2つ目のy軸設定
                title: {
                    text: '数量'
                },
                opposite: true // trueの場合グラフの右側にy軸を配置する
            }
        ],
        
        legend: {
            enabled: true
        },
        tooltip: {
            pointFormat: '受注額: <b>{point.y:.1f} 円</b>'
        },
        series: [{
            name: '受注額',
            type: 'column',
            data: [
                {% for a in amt %}
                    {{ a }} {% if not forloop.last %} , {% endif %}
                {% endfor %}
            ],
            dataLabels: {
                enabled: true,
                rotation: -90,
                color: '#FFFFFF',
                align: 'right',
                format: '{point.y:0f}', // one decimal
                y: 10, // 10 pixels down from the top
                style: {
                    fontSize: '13px',
                    fontFamily: 'Verdana, sans-serif'
                }
            },
            color:'green',
            yAxis: 0, // y軸を指定
            tooltip: {
                headerFormat: '<b>{point.x}</b><br/>',
                pointFormat: '{series.name}: {point.y:0,000.0f}円<br/>'
            },
        },
        {
            name: '数量',
            type: 'line', // グラフ種類 line column pie
            data: [
                {% for q in qut %}
                    {{ q }} {% if not forloop.last %} , {% endif %}
                {% endfor %}
            ],
            color:'darkred',
            yAxis: 1, // y軸を指定
            tooltip: {
                headerFormat: '<b>{point.x}</b><br/>',
                pointFormat: '{series.name}: {point.y:0,000.0f}<br/>'
            },
        }]
    });
</script>
~~~
冒頭の部分、{% load static %}　は静的なファイルを読み込むときに必要となるタグです。
その後で必要な機能の部分をロードすることになります。その部分はテーブル表示の記述の後あたりにあります。

~~~
<!--HightChartで必要な部品----------------------------------------------------------------------->
<script src="{% static '/highcharts/code/highcharts.src.js' %}"></script> <!--必須-->
<script src="{% static '/highcharts/code/modules/exporting.js' %}"></script> <!--右上メニュー：イメージ出力で必要-->
<script src="{% static '/highcharts/code/modules/export-data.js' %}"></script> <!--右上メニュー：データ出力で必要-->
<!--------------------------------------------------------------------------------------------->
~~~
３行で必要なスクリプトを込み込んでいます。src="{% static ・・・の部分で記述されてるstaticですが、フォルダの場所を示しています。したがってstaticが何処かを定義しなくてはいけないのですが、それは Settings.pyの中で行います。
settings.pyの一番下に追記して下さい。

settings.py

~~~
STATICFILES_DIRS=(
    os.path.join(BASE_DIR, 'static'),
)
~~~
BASE_DIRはsettings.pyの上の方に定義されていますが、プロジェクトのパスです。
スクリプトでは、staticフォルダの中にロードするファイルが有ることを期待しています。そしてそのstaticフォルダはプロジェクトファイルの中です。プロジェクトフォルダの直下に【static】フォルダを作成して下さい。そして中に【highcharts】を作成して下さい。

MYSITE
　├【items】
　├【mysyte】
　└【static】
　　　　└【highcharts】

フォルダが出来たら、Highchartsをダンロードします。
Highchartsは、商用利用でない場合はライセンスフリーのプログラムということでグラフ表示に利用させてもらいます。
　ダウンロード：<https://www.highcharts.com/blog/download/>
![キャプチャ111.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440325/c4b2e54f-d0a2-1af3-802b-24d408e95746.jpeg)

クリックするとダンロードが始まります。
ダウンロードが完了したら、中にはいっているファイルを【highcharts】フォルダの中に全部入れます。結果、下記の構成になると思います。

MYSITE
　├【items】
　├【mysyte】
　└【static】
　　　　└【highcharts】
　　　　　　　├【code】
　　　　　　　├【example】
　　　　　　　├【gfx】
　　　　　　　├【graphics】
　　　　　　　└api.zip

htmlの下半分でグラフの設定を行っていますが、{% %}や{{ }}で囲まれている箇所がDjangoの変数やテンプレートタグということになります。Highchartsが必要とする値をセットしています。

使用されているブラウザにもよりますが、IEの場合はスクリプトを有効にしないとグラフは表示されないようです。みなさんの環境でも動作するとよいのですが。。。

テンプレートとHighchartsの解説をかなり端折っている感じもありますが、実際に動かしてみます。

![キャプチャ2121.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440325/48229f55-7c38-9ec3-c5ce-004694380752.jpeg)

![キャプチャ333.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440325/bce74793-c235-ae9e-27b4-ff42b1751dc0.jpeg)

いかがでしょうか。うまく表示されているとよいのですが。。。
１０００件程度のデータ量なので、あっという間に表示されたと思います。環境にも依りますが、１００万件のデータでもそんなに時間はかからないのでは無いでしょうか。

###4.今回のまとめ
　受注明細のModelからDjangoのデータ操作をしてみましたが、抽出の場面でその機能の一部を紹介することが出来ました。
djangoのデータ操作は始めのうちはわかり辛いと思います。もしSQL文を使いたいときにはそれをつかってデータ操作することもできますし、抽出データをpythonのライブラリであるpandasに渡して加工することもできます。つまりデータを扱うための術が多岐に用意されていると思います。

###おわりに
　ここまで読んでいただきまして、ありがとうございました。Djangoについてなるべく実務的な観点から解説したかったのですが、解説すべき事項の漏れや誤記、その他プログラムの未熟さなどはどうぞご勘弁下さい。

最後に私がDjango学習にあたり主に教材にしたものを掲載させていただきます。

「現場で使えるDjangoの教科書」：<https://booth.pm/ja/items/1059917>
「simple is better than complex」：<https://simpleisbetterthancomplex.com/>

WEB開発は、django(restframework) + vueJS で自由度が向上すると思います。
　発展形はこちらから：<http://133.167.36.193/blog/post/42/>

以上