djangoをdockerコンテナで利用(5)- djangoでデータベースの読み書き

◆◆◆

ログインしないとページが見えないようなアプリケーションを構成し、データベースにある業務データを読み書きできるようにする。

(1.dockerコンテナ準備onSSL)(2.pip利用とhelloWorldとdb接続)(3.グラフ表示練習)では環境を作り、処理を作っていくための習作みたいなものまで作れた。

その後(4.ログイン画面)もできた。

次はcrudを目指す。djangoでmariadbを読み書き削除させる。ListView,DetailView,CreateView,DeleteView,UpdateViewをそれぞれ使う。phpで作った全機能はまだ無理で部分的やけど、いくらか使えるようになったのでその作成メモ。

いろんなページ参照させてもらいました。
作者のみなさんありがとう。

【準備】crudするためのデータベース

データの読み書き処理ではテストするときにデータをいくらか壊していく。
アプリケーションが無茶苦茶しても、データはいつでも元に戻せるようにしとかないとね。

自分の場合、オリジナルデータはgoogle cloudのlinuxで動かしているmariadbコンテナにあるから、そのコンテナの永続化領域をそのまま持ってきてローカルのmariadbのコンテナにアタッチして使ってる。

django-crud

djangoの管理画面が起動できるようにしたときに作られるテーブルは10個。「auth_」で始まるものと「django_」で始まるものがそれ。

これを丸々mysqldumpってコマンドでバックアップ取っておき、データベース名を書き換えて「nariDB_Django」のスキーマへリストアし、「GVIS_」で始まる業務テーブルをcreateしておく。

django-crud

nariDB_1stが業務テーブルだから、nariDB_Djangoに向かってa5sqlの「スキーマ間のデータ転送」を使ってデータを流し込めばいつでもデータがオリジナルと同じになる。

django-crud

a5sqlの作者さんありがとう。
このソフトウェアとても優秀。使ってないけど、なぜかテーブル定義書まで出せる。しかもけっこうキレイに出る。ポータブル版としても動くのでめっちゃ便利。

以前はoracleやsqlserverも扱うことがあったからnavicatを使ってたけど、今はmariadbだけなのでa5sqlで足りる。

navicatは有償で高価だけど、データの移行はめっちゃ楽。それはまた別の機会に。

オリジナルデータにはblobが入っているテーブルがあって、その中にjpegやpdfが入っている。だからコミットは1000件ずつじゃなく、100件ずつでのっそり動かす。トータル4GB程度なので、5分ぐらいで転送終わる。

サラッと書いてるけどコンテナで扱うからちょっと工夫が必要。
DBはまた別のページで書くか。

【準備】自前のcss

自前というか、以前に人にお願いして作ってもらったものを更新しながら利用。タブレットとかiphoneで見るためのおまじないのためでもある。

php版ではやってないけど、djangoではページネーションに挑戦。検索結果を表示するとき、たとえば1ページ20個レコード表示させておき、その他レコードは別ページでってことで。

ページネーションってこういう感じのやつ。

django-crud

これをやるにはcss使う。ググるとbootstrapを使うのが多い。自分は普通のページネーション装飾を参考にさせてもらい、ベース色を変更してcssに追記した。

実際はまだまだ作りこんでいくので、ページネーションする箇所は別htmlで切り出して使うようにした。

【結論】実際の動き

見た目の動きはこうなる。以下作った順。

ログイン

前回作った画面でログインする。イントラのサイトでオレオレ証明書使ってsslでつながってくれる。

django-crud

メニュー画面

未作成箇所あるけど、今回は真ん中のボタン「(作成中)資産一覧」ってとこ。他にも帳簿入力画面とか作っていくんだけど、crudの基本を資産登録機能で作っておいた。

django-crud

一覧表示

ページネーションで最終ページ表示させたところ。

この表示ができるようになるのは苦しんだ。djangoにはページ表示のために関数版とクラス版があることに気づき、関数版で作りかけてたのをクラス版でやり直した。modelsとformsを組み合わせて自動生成してくれるのもなかなか理解しづらかった。

php版で実現していることはそのまま実現できないので、まだまだデザイン変えてかないと。

最後の3件はテストで作成したレコード。

django-crud

詳細表示

今はのっぺりしてるけど、この機能を最後まで作ったらmariadbのテーブルのblob列に入ってる写真やマニュアルを閲覧するのに使うかな。

django-crud

新規作成

テストデータ入れて試してるところ。作ってる途中はフォームの扱いに苦しんだな。

django-crud

削除

確認メッセージは必要やね。このあたりで新規作成と更新画面の両方に確認メッセージと、完了メッセージの表示させ方考え始めた。

django-crud

更新

本当はこの画面の「Seq2」は入力項目じゃなくて自動発番させたい。レコードの中から最大値を得てインクリメントしたものを設定すればたぶんできるけど、今はこのままでいいか。

django-crud

更新だけじゃなく、新規作成もだけどjavascript使って確認メッセージ出るようにした。バリデーションとか入力画面を作ってくれるなら、このへんもdjangoでやってくれたらいいのに・・・。

django-crud

djangoの修正対象はけっこうある

今回もある程度できた後で、gitの差分を見ながら作った箇所書いてるのな。

アプリケーションとして以前に「gvisWebApp」を追加して自動生成された内容に、★印の箇所を編集、手動作成してった。

コメントに「参考URL」って入れてて、書くときに参考にさせてもらったページを書いてる。

それぞれの作者さんありがとう。

/code/app
|--gvisDjango3
|  (円グラフ等、ステータス表示のための別プログラムのため省略)
|--gvisWebApp
|  |--__init__.py
|  |--__pycache__
|  |--admin.py
|  |--apps.py
|  |--forms.py ★
|  |--migrations
|  |--models.py ★
|  |--tests.py
|  |--urls.py ★
|  |--views.py ★
|  |--templates/gvisWebApp          
|  |  |--base.html
|  |  |--gvis_001_IntraIchiran.html  ★手動作成
|  |  |--gvis_100_ShisanKensaku.html ★手動作成
|  |  |--gvis_101_ShisanDetail.html  ★手動作成
|  |  |--gvis_102_ShisanCreate.html  ★手動作成
|  |  |--gvis_103_ShisanDelete.html  ★手動作成
|  |  |--gvis_104_ShisanUpdate.html  ★手動作成
|  |  |--home.html
|  |  |--login.html
|  |  |--logout.html
|  |  |--pagination.html             ★手動作成
|--manage.py
|--requirements.txt 
|--templates
|  |--gvisDjango3
|  |  |--gvisDjango3Top.html
|  |  |--Header.html
|--website
|  |--__init__.py
|  |--__pycache__
|  |--asgi.py
|  |--settings.py
|  |--static
|  |  |--admin
|  |  |  |--commonGreen
|  |  |  |  |--css
|  |  |  |  |  |--gvisGreen.css  ★
|  |--urls.py
|  |--wsgi.py

修正内容

データ構成をどう使わせるか/見せるかを考えながら、models/forms/viewsあたりいじる。

viewは対応するテンプレートがあるから手動で作成してった。

models.pyの内容

このモデルは、以前に既存のテーブルへid列をプライマリキーとして追加した後、inspectさせて作ったものを下敷きにして作った。

GvisZaikoって名前は、利用している動産を管理するためのモデル。業務利用している有形無形のものをリスト化するために使ってる。

from datetime import datetime,date  ## テーブルの日付フィールドを表示させるため
from django.db import models
class GvisZaiko(models.Model):

    serialshubetsu_choices = (
            ('GVIS','GVIS-(製品登録資産したもの)'),
            ('GVI','GVI-私財投入資産'),
            ('GVH','GVH-ハードウェア資産'),
            ('GVS','GVS-ソフトウェア資産'),
            ('GVO','GVO-オープンソース資産'),
        )

    ## 参考URL https://note.nkmk.me/python-datetime-now-today/
    dt_year = datetime.now().year

    alwayson_choices = (
        (0,'0-Off'),
        (1,'1-On'),
    )

    serialseq1_choices = (
            (2014,'2014'),
            (2015,'2015'),
            (2016,'2016'),
            (2017,'2017'),
            (2018,'2018'),
            (2019,'2019'),
            (2020,'2020'),
            (2021,'2021'),
            (2022,'2022'),
            (2023,'2023'),
            (2024,'2024'),
            (2025,'2025'),
            (2026,'2026'),
            (2027,'2027'),
            (2028,'2028'),
            (2029,'2029'),
            (2030,'2030'),
            (2031,'2031'),
            (2032,'2032'),
            (2033,'2033'),
            (2034,'2034'),
            (2035,'2035'),
            (2036,'2036'),
            (2037,'2037'),
            (2038,'2038'),
            (2039,'2039'),
            (2040,'2040'),
        )

    serialshubetsu = models.CharField(verbose_name='資産種別',db_column='SerialShubetsu', max_length=4,
        choices=serialshubetsu_choices , default='GVH')  # Field name made lowercase.
    def __str__(self):
        return self.serialshubetsu

    serialseq1 = models.IntegerField(verbose_name='seq1',db_column='SerialSeq1', 
        choices=serialseq1_choices, default=dt_year )  # Field name made lowercase.
    def __str__(self):
        return str(self.serialseq1)

    serialseq2 = models.IntegerField(verbose_name='seq2',db_column='SerialSeq2',default=1 )  # Field name made lowercase.
    def __str__(self):
        return str(self.serialseq2)

    gvis_no = models.CharField(verbose_name='GVIS_No',db_column='GVIS_No', max_length=13, blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.gvis_no

    tehai = models.CharField(verbose_name='手配',db_column='Tehai', max_length=1000, blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.tehai

    category1 = models.CharField(verbose_name='カテゴリ1',db_column='Category1', max_length=100, blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.category1

    category2 = models.CharField(verbose_name='カテゴリ2',db_column='Category2', max_length=100, blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.category2

    alwayson = models.IntegerField(verbose_name='常時ON',blank=True, null=True,
        choices=alwayson_choices,default=0)
    def __str__(self):
        return self.alwayson

    class Meta:
        managed = False
        db_table = 'GVIS_zaiko'

modelの修正点はだいたいこんな感じ。最終的にはdate型/blob型を追加していくけど、今は文字と数字を入力できるようにし、ついでにリストボックス入力できるようにした。

  1. choicesを使う
    自分は最初のほうに、「カラム名_choices」で定義をまとめておき、使うカラムに例えば
    choices=serialshubetsu_choicesって書く。

    alwaysonのchoicesは今は使わないけど、次の修正で使う予定なので書いた。
  2. verbose_nameで画面表示用の定義
    ここに書いておくと、それだけで入力画面の項目名を添えてフィールド作ってくれるらしい。あら便利。
  3. defaultの設定
    default=0とか書いておく。

そういえばrailsで開発してるとき、nullとnillの違いってのがあったな。pythonはどうなんやろ。どっかでドキュメント読んどかなアカンなぁ。

mysqlをまだ使ってる頃、date型にnullが入れれてたけどmariadbはdate型にnullを許してくれなかったと思う。mariadbに引っ越しするとき、nullの代わりに”0000/00/00″みたいな日付を入れたような気がする。

djangoでdate型にdefault=nullとか書いたらcreateviewでexceptionでも出るんかなぁ。今度やってみる。

forms.pyの内容

追加は以下のとおり。modelsに書いたのと似たようなもんか。
入力画面のチェックとかできるらしいけど、リストボックスやからチェックも何もない。

from django import forms

## 参考URL  https://qiita.com/y_nishimura/items/1427a21967813de26237
##          https://codelab.website/django-form-initial/
class GvisZaikoForm(forms.Form):
    serialshubetsu = forms.ChoiceField(
        label='検索資産', widget=forms.widgets.Select,initial='',
        choices = (
            ('','-ALL-'),
            ('GVIS','GVIS-(製品登録資産したもの)'),
            ('GVI','GVI-私財投入資産'),
            ('GVH','GVH-ハードウェア資産'),
            ('GVS','GVS-ソフトウェア資産'),
            ('GVO','GVO-オープンソース資産'),
        ),
        required=False
    )
    serialseq1 = forms.ChoiceField(
        label='資産登録年', widget=forms.widgets.Select,initial='',
        choices = (
            ('','-ALL-'),
            ('2014','2014'),
            ('2015','2015'),
            ('2016','2016'),
            ('2017','2017'),
            ('2018','2018'),
            ('2019','2019'),
            ('2020','2020'),
            ('2021','2021'),
            ('2022','2022'),
            ('2023','2023'),
            ('2024','2024'),
            ('2025','2025'),
            ('2026','2026'),
            ('2027','2027'),
            ('2028','2028'),
            ('2029','2029'),
            ('2030','2030'),
        ),
        required=False
    )

urls.pyの内容

ここちゃんと書かないと「そんなページないで」とかすぐにエラーが出る。
パラメータの渡し方とか、ページの構成設計ちゃんと考えて書かなアカン。

まぁ、最初なので深く考えずにテーブルのidを渡してpostでパラメータしていくからそんなに複雑な書き方はしない。

"""website URL Configuration

参考URL
    https://qiita.com/hdj16802/items/fac57d86379d49560afa
    https://intellectual-curiosity.tokyo/2018/11/13/djangoのログイン処理を実装する方法①/

"""
from django.conf.urls import url
from django.contrib.auth import views as auth_views
from . import views
from django.urls import path, include

app_name = 'gvisWebApp'

urlpatterns = [
    path('login/', auth_views.LoginView.as_view(template_name='gvisWebApp/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(template_name='gvisWebApp/logout.html'), name='logout'),
    path('home/', views.home, name="home"),
    path('gvis_001_IntraIchiran/', views.gvis_001_IntraIchiran, name="gvis_001_IntraIchiran"), ## ★
    path('gvis_Complete/', views.gvis_Complete, name="gvis_Complete"),
    path('gvis_100_ShisanKensaku/', views.GvisZaikoList.as_view(), name="gvis_100_ShisanKensaku"),
    path('gvis_101_ShisanDetail/<int:pk>/', views.GvisZaikoDetail.as_view(), name="gvis_101_ShisanDetail"),
    path('gvis_102_ShisanCreate/', views.GvisZaikoCreate.as_view(), name="gvis_102_ShisanCreate"),
    path('gvis_103_ShisanDelete/<int:pk>/', views.GvisZaikoDelete.as_view(), name="gvis_103_ShisanDelete"),
    path('gvis_104_ShisanUpdate/<int:pk>/', views.GvisZaikoUpdate.as_view(), name="gvis_104_ShisanUpdate"),
]

変更点は「★」のある「gvis_001_IntraIchiran」より下の行。
一覧・詳細・新規作成・削除・更新の順に作成してった。

viewsでログイン画面はdefを使って関数で定義してるけど、「class使うほうがええで」ってどこかのページで読んだ。

一覧のページをdefで作りかけてたのを途中でclass定義に変えるのに何て書けばいいのかちょっと迷ったな。

「gvis_Complete」は先に用意したけど最終的には使わんかった。

views.pyの内容とテンプレート

viewsは長いので分割。

まずはインポート定義。django.views.generic importってある箇所あたりが今回の根幹かな。

from django.shortcuts import redirect, render         ## お決まりのインポート

from django.contrib.auth.decorators import login_required
                                            ## 関数でログイン必須にするため

from .models import GvisZaiko               ## DB読み込みのため

from django.views.generic import (
                            ListView,       ## リスト表示のため
                            DetailView,     ## 単一レコードの詳細情報表示のため
                            CreateView,     ## レコードの作成のため
                            DeleteView,     ## レコードの削除のため
                            UpdateView,     ## レコードの更新のため
)

from django.db.models import Q              ## 検索のため

from django.contrib.auth.mixins import LoginRequiredMixin
                                            ## クラスビューをログイン必須にするため
                                            ## 参考URL  https://e-tec-memo.herokuapp.com/article/62/
                                            ##          https://stackoverflow.com/questions/10275164/django-generic-views-using-decorator-login-required

from django.urls import reverse,reverse_lazy
                                            ## urlを返すreverse/reverse_lazyを使うため

from .forms import GvisZaikoForm            ## 検索フォームを使うため

from django.contrib import messages         ## 通知メッセージを表示するため

最後の3つのインポートについて。

GvisZaikoFormはformsを活用するための記述。

messagesは処理完了後にメッセージを表示させるためのもの。
小さな「×」ボタンをつけて閉じれそうに見えたけど、実際につけたら閉じれんかった。なんでやろ。今後の課題。

reverseを使った後、たとえば「閉じる」ってボタンを作って「onclick=windows.close」みたいなことを書いたら閉じれないことがあった。

自分の好みなんだけど、ある情報を開くときは「target=”_blank”」ってやりたくなる。

いつぐらいからか忘れたけど、ブラウザのタブを閉じるボタンでのクローズができなくなったなって思ってた。

ついでに調べたら解説してくれてる方がおられた。
作者さんありがとう。

【令和版】window.close() でタブが閉じない時の解決法 - Qiita
開発者「すいません、window.close()が動かないんですけど…」ワイ「コンソールになんか書いてないですか?」Scripts may close only the windows that…

「JavaScriptで閉じることができるウィンドウは、JavaScriptで開かれたウィンドウのみ」ってことで禁じるようになったんやなぁ。

詳細画面と新規作成画面を開くときは新規タブで開いておき、編集と削除はreverse_lazy使うようにした。

ログイン画面

関数記述のhtmlを表示させるだけの軽い処理。前に作ったログインとログオフの箇所はそのまま。

画面遷移の試行錯誤の途中でgvis_Completeは作ったけど、messagesってのを使ってみたくなって、最終的に使わなくなった。

@login_required
def home(request):
    return render(request, 'gvisWebApp/home.html')

@login_required
def gvis_001_IntraIchiran(request):
    return render(request, 'gvisWebApp/gvis_001_IntraIchiran.html')

@login_required
def gvis_Complete(request):
    return render(request, 'gvisWebApp/gvis_Complete.html')

一覧(GvisZaikoList)

リスト表示。
新規作成・編集・詳細・削除を呼び出したり、mariadbから検索条件指定してレコード取ってくるのでちょっとリスト長い。

モデルの名前に由来するように書けば、テンプレート名は省略できるらしいけど、できるだけちゃんと書きたくなってしまうので書いてる。

続いて3つのdef処理を書いてる。これらのdefはお決まりの定義らしくて、自分で勝手に名前をつけるものではない。

1つ目のdef postではformsで定義した検索項目の値をセッションに渡す。

class GvisZaikoList(LoginRequiredMixin, ListView):

    model = GvisZaiko
    template_name = 'gvisWebApp/gvis_100_ShisanKensaku.html'
    paginate_by = 20

    ## 参考URL https://intellectual-curiosity.tokyo/2019/02/27/djangoでlistviewを用いて検索画面を実装する方法/
    def post(self, request, *args, **kwargs):
        form_value = [
            self.request.POST.get('serialshubetsu', None),
            self.request.POST.get('serialseq1', None),
        ]
        request.session['form_value'] = form_value

        # 検索時にページネーションに関連したエラーを防ぐ
        self.request.GET = self.request.GET.copy()
        self.request.GET.clear()
        return self.get(request, *args, **kwargs)

2つ目のdef get_context_dataはセッションから検索フォームの値を取得する。

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # sessionに値がある場合、その値をセットする。(ページングしてもform値が変わらないように)
        serialshubetsu = ''
        serialseq1 = ''
        if 'form_value' in self.request.session:
            form_value = self.request.session['form_value']
            serialshubetsu = form_value[0]
            serialseq1 = form_value[1]
        default_data = {'serialshubetsu': serialshubetsu,   # シリアル種別
                        'serialseq1': serialseq1,           # 年
                        }
        find_form = GvisZaikoForm(initial=default_data) # 検索フォーム
        context['find_form'] = find_form
        return context

3つ目のdef get_querysetは検索フォームの値に応じてクエリ発行を行う。
「Q」っていうのをインポートしておき、クエリ発行は特有の書き方する。

ここでは画面上のリストボックスで「シリアル種別」と「年」を指定したときで検索の仕方を変えてる。

元SQLをベースにして検索条件やソートの指定を探しながら書いた。やっぱり慣れへんからSQLで考えてから翻訳するみたいなことやってまうな。

    ## 参考URL  https://noumenon-th.net/programming/2019/12/18/django-search/
    ##          https://codelab.website/django-queryset-filter/
    ##          https://qiita.com/Hyperion13fleet/items/1a0369f4f5d523be5870
    def get_queryset(self):

        # sessionに値がある場合、その値でクエリ発行する。
        # シリアル種別はicontains(部分一致の大文字小文字区別なし)で検索する
        if 'form_value' in self.request.session:
            form_value = self.request.session['form_value']

            serialshubetsu = form_value[0]
            serialseq1 = form_value[1]

            if (len(serialshubetsu) > 0 and len(serialseq1) > 0):
                if (serialshubetsu == 'GVIS'):

                    ## from search SQL strings
                    ## " select * from GVIS_zaiko where `GVIS_No` like 'GVIS%' and SerialSeq1 = " . $arg3
                    ## " order by GVIS_No,SerialShubetsu,SerialSeq1,SerialSeq2 desc "
                    object_list = GvisZaiko.objects.filter(
                        Q(gvis_no__startswith='GVIS') & Q(serialseq1__contains=serialseq1)
                    ).order_by('gvis_no','serialshubetsu','serialseq1','-serialseq2')

                else:
                    ## from search SQL strings
                    ## " select * from GVIS_zaiko where SerialShubetsu = '" . $arg2 . "' and SerialSeq1 = " . $arg3
                    ## " order by SerialSeq1,SerialSeq2 desc "
                    object_list = GvisZaiko.objects.filter(
                        Q(serialshubetsu__iexact=serialshubetsu) & Q(serialseq1__iexact=serialseq1)
                    ).order_by('serialseq1','-serialseq2')

            elif (len(serialshubetsu) > 0):
                if (serialshubetsu == 'GVIS'):
                    ## from search SQL strings
                    ## " select * from GVIS_zaiko where `GVIS_No` like 'GVIS%' "
                    ## " order by GVIS_No,SerialShubetsu,SerialSeq1,SerialSeq2 desc "
                    object_list = GvisZaiko.objects.filter(
                        Q(gvis_no__startswith='GVIS')
                    ).order_by('gvis_no','serialshubetsu','serialseq1','-serialseq2')

                else:
                    object_list = GvisZaiko.objects.filter(
                        Q(serialshubetsu__iexact=serialshubetsu)
                    ).order_by('serialseq1','-serialseq2')

            elif (len(serialseq1) > 0):
                object_list = GvisZaiko.objects.filter(
                    Q(serialseq1__iexact=serialseq1)
                ).order_by('serialseq1','-serialseq2')
            else:
                object_list = GvisZaiko.objects.all().order_by('serialseq1','-serialseq2')

            return object_list
        else:
            # 何も返さない
            return GvisZaiko.objects.none()

一覧(テンプレート gvis_100_ShisanKensaku.html)を手動作成

検索で使う項目はviewsに「def get_context_data」で定義したfind_formを受け取って表示させてる。

検索結果はviewsでページネーション利用「paginate_by」としてるので、検索結果にページネート用のhtmlがインクルードされてレコードと一緒に表示されるようになっている。

レコードはmodelsで定義した名前をviewsで定義して検索結果を格納したobject_listで受け取って表示させてる。

レコード1つ1つに「詳細」「編集」「削除」のリンクをつけておき、最下段には「新規登録」のボタンで新規作成ができるようにしている。

ページへのリンクは「アプリ名:クラス名」を書いて「gvisWebApp:gvis_104_ShisanUpdate」って具合に入れる。

ダークモード使いたいから「class=”darkmode-ignore” 」は、cssにダークモード設定入っているので、ボタンの色変化を除外したいので書いてある。

最後にある「messages」は、viewsで受け取ったメッセージがあれば表示させる。メッセージが複数の場合もあるのでforで回してる。

{% extends 'gvisWebApp/base.html' %}
{% load static %}

{% block content %}
<center>
    {# 参考URL #}
    {# https://noumenon-th.net/programming/2019/12/18/django-search/ #}

     <h1>検索条件</h1>
    <form method="POST">
        {% csrf_token %}

        <fieldset style="width:60%">
          <legend align="center">検索フォーム</legend>

          {% for field in find_form %}
            {{ field.label }}:{{ field }}
            <br>
          {% endfor %}
          <input class="gvis_button" type="submit" id="submit1" name="submit1" value="検索">
        </fieldset>
    </form>
    <br>

    <h1>検索結果</h1>
    {% if object_list|length == 0 %}
        <p>検索結果が存在しません。</p>
    {% else %}

        {% if is_paginated %}
            {% include 'gvisWebApp/pagination.html' %}
        {% endif %}

        <table border=4 align=center>
          <tr>
            <th>編集</th>
            <th>シリアル</th>
            <th>GVIS_No</th>
            <th>手配</th>
          </tr>
          {% for GvisZaiko in object_list %}
          <tr>
            <td>
              <a target="_blank" href="{% url 'gvisWebApp:gvis_101_ShisanDetail' GvisZaiko.pk %}">詳細</a>
              <a>  <a>
              <a href="{% url 'gvisWebApp:gvis_104_ShisanUpdate' GvisZaiko.pk %}">編集</a>
              <a>  <a>
              <a href="{% url 'gvisWebApp:gvis_103_ShisanDelete' GvisZaiko.pk %}">削除</a></td>
              <a>  <a>
            </td>
            <td>{{GvisZaiko.serialshubetsu}}{{GvisZaiko.serialseq1}}-{{GvisZaiko.serialseq2}}</td>
            <td>{{GvisZaiko.gvis_no}}</td>
            <td>{{GvisZaiko.tehai}}</td>
          </tr>
          {% endfor %} 
        </table>

    {% endif %}

    <br>
    <input class="darkmode-ignore" type="button" id="button1" name="button1" onclick="window.open('{% url 'gvisWebApp:gvis_102_ShisanCreate' %}' );" value="新規登録" >

    {% if messages %}
      {% for message in messages %}
        <h2> {{ message }}</h2>
      {% endfor %}
    {% endif %}

</center>

{% endblock content %}

詳細(GvisZaikoDetail)

検索画面のリンクから呼び出すと、新規タブとして表示されるようにしている。
複数ディスプレイでタブをウィンドウの外にドラッグしたら独立したウィンドウになって見比べができるから、新規タブで開いてくれるほうがいい。

viewにはたったこれだけ、めっちゃシンプル。
表示のさせ方はテンプレートの中に書く。

class GvisZaikoDetail(LoginRequiredMixin, DetailView):

    ## 参考URL  https://noumenon-th.net/programming/2019/11/16/django-detailview/
    ##          https://di-acc2.com/programming/python/5926/#index_id10
    model = GvisZaiko
    template_name = 'gvisWebApp/gvis_101_ShisanDetail.html'
    context_object_name = "GvisZaiko"

詳細(テンプレート gvis_101_ShisanDetail.html)を手動作成

modelsに定義された内容を普通に表示させる。
「ウィンドウを閉じる」のボタンを置いてタブが閉じられるようにしてる。

{% extends 'gvisWebApp/base.html' %}
{% load static %}

{% block content %}
<center>
    {# 参考URL #}
    {# https://noumenon-th.net/programming/2019/12/18/django-search/ #}

    <table border=4 align=center>
      <tr>
        <th>項目/th>
        <th>値</th>
      </tr>
      <tr>
        <td>シリアル種別</td>
        <td>{{GvisZaiko.serialshubetsu}}</td>
      </tr>
      <tr>
        <td>seq1</td>
        <td>{{GvisZaiko.serialseq1}}</td>
      </tr>
      <tr>
        <td>seq2</td>
        <td>{{GvisZaiko.serialseq2}}</td>
      </tr>
      <tr>
        <td>手配</td>
        <td>{{GvisZaiko.tehai}}</td>
      </tr>
      <tr>
        <td>カテゴリ1</td>
        <td>{{GvisZaiko.category1}}</td>
      </tr>
      <tr>
        <td>カテゴリ2</td>
        <td>{{GvisZaiko.category2}}</td>
      </tr>
    </table>
    <br>
    <input class="darkmode-ignore" type="button" name="close_btn" onclick="window.close();" value="ウィンドウを閉じる" >

</center>

{% endblock content %}

新規作成(GvisZaikoCreate)

ここも新規タブとして開かれる。他の詳細タブと見比べながら入力することが多いから。

fieldsにはmodelsで定義しておいた利用カラム名を書く。
全部使うときは省略できるらしいけど、やっぱり明示的に書きたいので書いた。

def get_success_urlはバリデーションがうまくいって保存するときの動きを定義する。

保存ができたらreverse使って詳細画面に渡して表示させてる。

def form_invalidはバリデーションがうまくいってないときの動きを定義するらしい。

messagesを使うと画面の下のほうにメッセージ表示してくれる。
検索ボタンを押してクエリ動かしなおすと表示が消える。

なんとこのメッセージを表示させた状態でhtmlソース見ると何も記述がない。
なんでやろ。
イマイチ理解できてへんな。

class GvisZaikoCreate(LoginRequiredMixin,CreateView):

    ## 参考URL  https://noumenon-th.net/programming/2019/11/18/django-createview/
    ##          https://di-acc2.com/programming/python/5926/#index_id10
    ##          https://qiita.com/okoppe8/items/b8b110803a89b09b00bd
    ##          https://dot-blog.jp/news/django-messages-frame-work/

    model = GvisZaiko
    template_name = 'gvisWebApp/gvis_102_ShisanCreate.html'
    fields = [
        'serialshubetsu',
        'serialseq1',
        'serialseq2',
        'tehai',
        'category1','category2',
        ]

##    https://igreks.jp/dev/django-createviewやupdateview利用時、任意のデータを裏側で保存する/

    def get_success_url(self):
        messages.success(self.request,'登録内容を保存しました。')
        return reverse('gvisWebApp:gvis_101_ShisanDetail', kwargs={'pk': self.object.pk})

    def form_invalid(self, form):
        messages.error(self.request,'入力に誤りがあります。登録に失敗しました。')
        return super().form_invalid(form)

新規作成(テンプレート gvis_102_ShisanCreate.html)を手動作成

「form.as_p」の「p」はhtmlの<p>のことらしい。
tableのほうが好みなので、「form.as_table」ってやったら一覧形式になってくれた。

解説されているサイトはたいてい保存機能のみで終わってるんだけど、確認メッセージが欲しかった。

入力フォームの定義にonsubmit="return kakuninPopup()って書いといて簡単なjavascript呼び出すようにし、結果がtrueのときに保存されるようにした。

ほんの少しのことなんだけど、モーダル画面作るややこしいやり方しか見つけられず、javascriptでできるってことでシンプルに作った。

onclickじゃなくてonsubmit使わないと思った動きにならない。

保存したら、呼び出し元の画面をリロードするjavascriptがあるみたいだけど今はまだ入れてない。

{% extends 'gvisWebApp/base.html' %}
{% load static %}

{% block content %}
<center>
    {# 参考URL #}
    {# https://noumenon-th.net/programming/2019/11/18/django-createview/ #}
    {# https://qiita.com/hdj16802/items/51b4156620e13f68a696 #}
    {# https://chusotsu-program.com/form-submit-confirm/ #}


    <h1>資産新規登録</h1>
    <form action="{% url 'gvisWebApp:gvis_102_ShisanCreate' %}" method="post" onsubmit="return kakuninPopup()">
        {% csrf_token %}
        <table border=4 align=center>
        {{ form.as_table }}
        </table>
        <br>
        <input class="darkmode-ignore" type="submit" value=" 保 存 ">
    </form>

    <br>
    <input class="darkmode-ignore" type="button" name="close_btn" onclick="window.close();" value=" やめる " >

    {% if messages %}
      {% for message in messages %}
        <h2> {{ message }}</h2>
      {% endfor %}
    {% endif %}

</center>

<script type="text/javascript">
  {# formのonclickに渡すとキャンセルボタンを押してもtrueが戻ってしまうので注意 #}
  function kakuninPopup() {
    if(window.confirm('この内容で保存しますか?')) {
      return true;
    } else {
      return false;
    }
  }

</script>

{% endblock content %}

削除(GvisZaikoDelete)

削除画面は対象を表示させた確認画面で「削除する」ボタンが押されたら削除する。

def get_success_urlではメッセージを表示させ、reverse_lazy使って一覧(GvisZaikoList)に戻る。

このときセッションにある情報を再検索してくれるので、レコードは消えた状態が見える。

class GvisZaikoDelete(LoginRequiredMixin, DeleteView):

    ## 参考URL  https://noumenon-th.net/programming/2019/11/20/django-deleteview/
    model = GvisZaiko
    template_name = 'gvisWebApp/gvis_103_ShisanDelete.html'
    context_object_name = "GvisZaiko"

    def get_success_url(self):
        messages.success(self.request,'登録内容を削除しました。')
        return reverse_lazy('gvisWebApp:gvis_100_ShisanKensaku')

削除(テンプレート gvis_103_ShisanDelete.html)を手動作成

削除対象を表示させて、実際にレコード消すかどうか選ばせるための画面。
「history.back」っての使うと呼び出し元の一覧画面に戻る。

{% extends 'gvisWebApp/base.html' %}
{% load static %}

{% block content %}
<center>
    {# 参考URL #}
    {# https://noumenon-th.net/programming/2019/11/20/django-deleteview/ #}

    <h1>削除確認画面</h1>
    <form method="post">
      {% csrf_token %}
      <h2>削除しますか?データは元に戻せません。</h2>
      <table border=4 align=center>
        <tr>
          <th>項目/th>
          <th>値</th>
        </tr>
        <tr>
          <td>シリアル種別</td>
          <td>{{GvisZaiko.serialshubetsu}}</td>
        </tr>
        <tr>
          <td>seq1</td>
          <td>{{GvisZaiko.serialseq1}}</td>
        </tr>
        <tr>
          <td>seq2</td>
          <td>{{GvisZaiko.serialseq2}}</td>
        </tr>
        <tr>
          <td>手配</td>
          <td>{{GvisZaiko.tehai}}</td>
        </tr>
        <tr>
          <td>カテゴリ1</td>
          <td>{{GvisZaiko.category1}}</td>
        </tr>
        <tr>
          <td>カテゴリ2</td>
          <td>{{GvisZaiko.category2}}</td>
        </tr>
      </table>
      <br>
      <input class="darkmode-ignore" type="submit" value="削除する">
      <a> <a>
      <input class="darkmode-ignore" type="button" name="close_btn" onclick="history.back();" value=" やめる " >
    </form>    
    <br>

</center>

{% endblock content %}

更新(GvisZaikoUpdate)

新規作成とあまり変わらない内容。

class GvisZaikoUpdate(LoginRequiredMixin, UpdateView):

    ## 参考URL  https://noumenon-th.net/programming/2019/11/19/django-updateview/
    model = GvisZaiko
    template_name = 'gvisWebApp/gvis_104_ShisanUpdate.html'
    context_object_name = "GvisZaiko"
    fields = [
        'serialshubetsu',
        'serialseq1',
        'serialseq2',
        'tehai',
        'category1','category2',
        ]

    def get_success_url(self):
        messages.success(self.request,'登録内容を更新しました。')
        return reverse_lazy('gvisWebApp:gvis_100_ShisanKensaku')

    def form_invalid(self, form):
        messages.error(self.request,'入力に誤りがあります。登録に失敗しました。')
        return super().form_invalid(form)

更新(テンプレート gvis_104_ShisanUpdate.html)を手動作成

新規作成画面で使ったjavascriptのポップアップメッセージを少し変えて表示させつつ、更新処理させるための画面。

保存しても、やめても一覧画面に戻る。

{% extends 'gvisWebApp/base.html' %}
{% load static %}

{% block content %}
<center>
    {# 参考URL #}
    {# https://noumenon-th.net/programming/2019/11/19/django-updateview/ #}

    <h1>資産更新</h1>
    <form method="post" onsubmit="return kakuninPopup()">
        {% csrf_token %}
        <table border=4 align=center>
        {{ form.as_table }}
        </table>
        <br>
        <input class="darkmode-ignore" type="submit" value=" 保 存 ">
    </form>

    <br>
    <input class="darkmode-ignore" type="button" name="close_btn" onclick="history.back();" value=" やめる " >

    {% if messages %}
      {% for message in messages %}
        <h2> {{ message }}</h2>
      {% endfor %}
    {% endif %}

</center>

<script type="text/javascript">
  {# formのonclickに渡すとキャンセルボタンを押してもtrueが戻ってしまうので注意 #}
  function kakuninPopup() {
    if(window.confirm('この内容で保存しますか?')) {
      return true;
    } else {
      return false;
    }
  }

</script>
{% endblock content %}

詳細(テンプレート gvis_Complete.html)を手動作成

messageでメッセージが表示できることがわかったので使わないけど、事前に準備した完了後用の画面。

{% extends 'gvisWebApp/base.html' %}
{% load static %}

{% block content %}
<center>

  <h1>処理完了しました。ブラウザの×ボタン(or ctrl + w)でページを閉じてください</h1>
  <h1>検索画面に戻ったら再検索してください。</h1>
  <br>
  <input class="darkmode-ignore" type="button" name="close_btn" onclick="window.close();" value="ウィンドウを閉じる" >

  {% if messages %}
    {% for message in messages %}
      <h2> {{ message }}</h2>
    {% endfor %}
  {% endif %}

</center>


{% endblock content %}

詳細(テンプレート gvis_Complete.html)を手動作成

ページ送りボタン表示のためのhtml。
これから改造するとしたら、「先頭」と「最後」のボタン作るってとこか。
今はいらない。

classに書いている名前はcssに定義した名前を使っている。

<div class="darkmode-ignore">
    <div class="cp_navi">
        <div class="cp_pagination">
            <nav aria-label="ページ送り">
                {% if page_obj.has_previous %}
                    <a class="cp_pagenum prev" href="?page={{ page_obj.previous_page_number }}">&laquo;</a>
                {% else %}
                    <span class="cp_pagenum current">&laquo;</span>
                {% endif %}
                {% for i in page_obj.paginator.page_range %}
                    {% if page_obj.number == i %}
                        <a class="cp_pagenum current" href="#">{{ i }}</a>
                    {% else %}
                        <a class="cp_pagenum" href="?page={{ i }}">{{ i }}</a>
                    {% endif %}
                {% endfor %}
                {% if page_obj.has_next %}
                    <a class="cp_pagenum next" href="?page={{ page_obj.next_page_number }}">&raquo;</a>
                {% else %}
                    <span class="cp_pagenum current">&raquo;</span>
                {% endif %}
            </nav>
        </div>
    </div>
</div>

gvisGreen.cssの追記

cssは苦手。参考ページの内容はページ送りボタンのデザインが入っていて、ほぼコピペさせてもらった。使うときはhtml内の「class=」に書いた。

cssデザインできる人はエラいなぁって思ってしまう。やっぱり自分では参考サイトからの切り貼りか、色を変更するような簡単な改造しかできん。

/*==================================================
  pager用
  参考URL https://copypet.jp/980/
  ====================================================*/
  nav.cp_navi *, nav.cp_navi *:after, nav.cp_navi *:before {
    -webkit-box-sizing: border-box;
            box-sizing: border-box;
  }
  nav.cp_navi a {
    text-decoration: none;
  }
  nav.cp_navi {
    margin: 2em 0;
    text-align: center;
  }
  .cp_navi .cp_pagination {
    display: inline-block;
    margin-top: 2em;
    padding: 0 0.5em;
    background-color: green;
  }
  .cp_navi .cp_pagenum {
    font-size: 1em;
    line-height: 2.5em;
    display: block;
    float: left;
    padding: 0 25px;
    transition: 400ms ease;
    letter-spacing: 0.1em;
    color: #ffffff;
  }
  .cp_navi .cp_pagenum:hover,
  .cp_navi .cp_pagenum.current {
    font-weight: bold;
    color: #ffffff;
    background-color: #C5E1A5;
  }
  .cp_navi .cp_pagenum.prev:hover,
  .cp_navi .cp_pagenum.next:hover {
    color: #C5E1A5;
    background-color: transparent;
  }
  @media only screen and (max-width: 960px) {
    .cp_navi .cp_pagination {
      margin-top: 50px;
      padding: 0 10px;
    }
    .cp_navi .cp_pagenum {
    font-size: 0.8em;
    line-height: 2.5em;
    padding: 0 15px;
    }
    .cp_navi .cp_pagenum.prev,
    .cp_navi .cp_pagenum.next {
      padding: 0 10px;
    }
  }
  @media only screen and (min-width: 120px) and (max-width: 767px) {
    .cp_navi .cp_pagenum {
    display: none;
    padding: 0 14px;
    }
    .cp_navi .cp_pagenum:nth-of-type(2) {
    position: relative;
    padding-right: 50px;
    }
    .cp_navi .cp_pagenum:nth-of-type(2)::after {
    font-size: 1.2em;
    position: absolute;
    top: 0;
    left: 45px;
    content: '...';
    }
    .cp_navi .cp_pagenum:nth-child(-n+3),
    .cp_navi .cp_pagenum:nth-last-child(-n+3) {
      display: block;
    }
    .cp_navi .cp_pagenum:nth-last-child(-n+4) {
      padding-right: 14px;
    }
    .cp_navi .cp_pagenum:nth-last-child(-n+4)::after {
      content: none;
    }
    .cp_navi .cp_pagenum.prev,
    .cp_navi .cp_pagenum.next {
      padding: 0 5px;
    }
  }

その他わかってない点

たとえばmodels.pyを更新したらmakemigrationsさせなきゃいけないのはわかる。

思い違いしてるかもしれないけど、テンプレートのhtmlやviews.pyのインポート対象を更新したら、djangoのdockerコンテナの再起動が必要だった。

でも表示文字を変えるぐらいのテンプレートhtml修正だったり、viewの中の処理記述だったらコンテナの再起動しなくても表示できてたこともあった。

どういうときにdockerコンテナ再起動が必要になるのかイマイチわからない・・・。

まだ作り始めなのでコンテナ再起動は数秒で済んでるけど、これからどうなるんだろ。

次は何するか

ここまでほぼ毎日2時間を3週間ぐらいかけてやっとできた。
入力項目まだ実装終わってないものがあるから、まだ次の3週間は改造し続けないといけないな。

詰め込みが進むとviews.pyが大きくなりそうなので、どっかで分割することも考えないといけない。

タイトルとURLをコピーしました