haku-maiのブログ

インフラエンジニアですが、アプリも作ります。

【Django3.1】generic viewを使わずにfunctionを利用してCRUDを行う方法

本記事で行うこと

※なお通常Djangoでアプリケーションを作成する際は、generic viewを使い、機能を拡張したい時はfunctionをオーバーライドすることが自分は多い。
余談だが、generic viewが持っているfunctionは以下を参照すると良い。
Django Class-Based-View Inspector -- Classy CBV

なお本記事のソースコードgithub上に公開しています。
GitHub - n-guitar/django-samples at sample.function-view

モチベーション

  • Djangoを教える時のサンプルとして利用したいため。

環境

$ sw_vers
ProductName:    macOS
ProductVersion: 11.1
BuildVersion:   20C69
$ python -V
Python 3.8.0

事前準備 pyenv環境

$ python -m venv env
$ source env/bin/activate
$ export PATH=$PWD/env/bin:$PATH

Djangoのインストール

$ pip install django==3.1.3

Django 初期設定

  • プロジェクトとアプリを作成
  • プロジェクトでtempletsフォルダを設定
$ django-admin startproject confug .
$ django-admin startapp samplecrud
$ mkdir templates
  • settings.py編集 appの追加とtemplateフォルダを参照するように編集

confug/settings.py

・・・
# samplecrudを追加
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'samplecrud', # 追加
]
・・・
# TEMPLATESのDIRSを変更
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'], # 変更
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

動作確認

$ python manage.py runserver

http://127.0.0.1:8000/ にアクセスして以下が表示されればOK f:id:n-guitar:20201228201112p:plain:w600
以上で準備完了。
一旦control + cで止めておく。

現時点で以下のようなディレクトリ構成になっている。
f:id:n-guitar:20201228201643p:plain:w300

CRUD用のDBを作成

今回は以下のようなDBを作成する

Column Field
title CharField(max_length=100)
memo TextField(max_length=255)

samplecrud/models.py

from django.db import models

class Todo(models.Model):
    title = models.CharField(max_length=100)
    memo = models.TextField(max_length=255)

    def __str__(self):
            return self.title

DBの反映

$ python manage.py makemigrations samplecrud
Migrations for 'samplecrud':
  samplecrud/migrations/0001_initial.py
    - Create model Todo

$ python manage.py migrate samplecrud 
Operations to perform:
  Apply all migrations: samplecrud
Running migrations:
  Applying samplecrud.0001_initial... OK

Read画面の作成

早速Read画面を作成するがその前に、URLのつなぎこみを行う。 ※直接configからURLを指定してもいいが、通常アプリごとに作成する多い。

URLのつなぎこみ

$ touch samplecrud/urls.py

現時点のディレクトリ構造は以下の通りになっている。 f:id:n-guitar:20201228202723p:plain:w300

URLのつなぎこみ。
confug/urls.pyを編集する

from django.contrib import admin
from django.urls import path, include # include追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('samplecrud/', include('samplecrud.urls')), # 追加
]

上記のURLは『http://127.0.0.1:8000/samplecrud/』でアクセスした時、『samplecrud/urls.py』記載されているURLにルーティングするという意味。

samplecrud/urls.pyを編集する

from django.urls import path
from .views import list_page

urlpatterns = [
    path('list/', list_page, name='list'),
]

Read画面の作成

いよいよ画面を作成していく。

samplecrud/views.py

from django.shortcuts import render
from .models import Todo

def list_page(request):
    object_list = Todo.objects.all()
    return render(request, 'list.html', {'object_list':object_list})

object_list = Todo.objects.all()
はToDo Tableをすべて(all)取得し、object_listに格納している。
DjangoではSQLではなくORMを使いdatabaseを操作することが多い。
(SQLを利用することも可能だが、SQLインジェクション対策を考えなくてはいけなくなるので、特別な理由がなければORMを使うほうが良い。)
※ORMの詳細はマニュアルを参照
Making queries | Django documentation | Django

return render(request, 'list.html', {'object_list':object_list})
はlist.htmlに先程格納したobject_listを辞書型で渡し、レンダリングしている。

レンダリング先のhtmlがないので作成する。
templates/list.htmlの作成

<h1>list Page</h1>
<ul>
    {% for item in object_list %}
        <li>{{ item.title }}: {{ item.memo }}</li>
    {% endfor %}
</ul>

Todo tableから取得した値がobject_listというオブジェクトに格納され、list.htmlに渡される

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/list/ にアクセスする

以下のように表示されればOK。
今はテーブルに何もレコードがないため、何も表示されない。 f:id:n-guitar:20201228210609p:plain:w300

Create画面の作成

続けてDBのレコードを追加する画面を作成する。
以下のファイルをそれぞれ編集する。

samplecrud/urls.py

from django.urls import path
from .views import list_page, create_page # 追記

urlpatterns = [
    path('list/', list_page, name='list'),
    path('create/', create_page, name='create'), # 追記
]

こちらは先程のlistと同じ

samplecrud/views.py

from django.shortcuts import render, redirect
from .models import Todo

def list_page(request):
    object_list = Todo.objects.all()
    return render(request, 'list.html', {'object_list':object_list})

def create_page(request):
    if request.method == 'POST':
        title = request.POST['title']
        memo = request.POST['memo']
        Todo.objects.create(title=title, memo=memo)
        return redirect('list')
    return render(request, 'create.html')

createのときはどのGETとPOSTメソッドを処理する必要がある。
GETの時はCreate画面を表示し、POSTの時はformに入力された値を受け取ってdatabaseを更新させる必要がある。
Todo.objects.create(title=title, memo=memo) は受け取ったパラメータでToDo tableにレコード追加している。

※通常databaseを更新する時はバリデーションをすべきであるが本記事ではわかりにくくなるので行わないでおく。

templates/create.html

<h1>Create Page</h1>

<form action="" method="POST"> {% csrf_token %}
    <p>
        <label for="id_title">Title:</label>
        <input type="text" name="title" maxlength="100" required="" id="id_title">
    </p>
    <p>
        <label for="id_memo">Memo:</label>
        <textarea name="memo" cols="40" rows="10" maxlength="255" required="" id="id_memo"></textarea>
    </p>
    <input type="submit" value="作成する">
</form>

</form>

inputタグにname="※POSTで受け取りたい変数名"を入れることでviews.pyでその変数名で受け取ることができる。
Djangoは『 {% csrf_token %}』をつけることでデフォルトでCSRFの検証を行ってくれる。(逆につけないとエラーになる)

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/create/ にアクセスすると以下によう表示され、更新するとlistが表示されて先程更新したデータが表示される。
何件か登録してみるとすべて表示されることがわかる。

f:id:n-guitar:20201228212434p:plain:w300
f:id:n-guitar:20201228212920p:plain:w300

Update画面の作成

続けてDBのレコードを更新する画面を作成する。
以下のファイルをそれぞれ編集する。

samplecrud/urls.py

from django.urls import path
from .views import list_page, create_page, update_page

urlpatterns = [
    path('list/', list_page, name='list'),
    path('create/', create_page, name='create'),
    path('update/<int:pk>', update_page, name='update'),
]

こちらは先程と若干異なり、URL Pathに/<int:pk>が記載されている。
これはDBのどのレコードを更新していいか判断できないので<int:pk>で一意にレコードを特定する。
DjangoではデフォルトでTableを作成するとidというPrimary KeyになるColumnが作成される。

samplecrud/views.py

from django.shortcuts import render, redirect
from .models import Todo

def list_page(request):
    object_list = Todo.objects.all()
    return render(request, 'list.html', {'object_list':object_list})

def create_page(request):
    if request.method == 'POST':
        title = request.POST['title']
        memo = request.POST['memo']
        Todo.objects.create(title=title, memo=memo)
        return redirect('list')
    return render(request, 'create.html')

def update_page(request, pk):
    if request.method == 'POST':
        title = request.POST['title']
        memo = request.POST['memo']
        Todo.objects.filter(pk=pk).update(title=title, memo=memo)
        return redirect('list')
    object = Todo.objects.get(pk=pk)
    return render(request, 'update.html', {'object':object})

updateではどのレコードを表示、更新するか特定する必要がある。
object = Todo.objects.get(pk=pk) はPrimary Keyの値を条件にレコードを取得する。

Todo.objects.filter(pk=pk).update(title=title, memo=memo) はPrimary Keyでフィルターし更新する。

templates/update.html

<h1>Update Page</h1>

<form action="" method="POST"> {% csrf_token %}
    <p>
        <label for="id_title">Title:</label>
        <input type="text" name="title" value="{{ object.title }}" maxlength="100" required="" id="id_title">
    </p>
    <p>
        <label for="id_memo">Memo:</label>
        <textarea name="memo" cols="40" rows="10" maxlength="255" required="" id="id_memo">{{ object.memo }}</textarea>
    </p>
    <input type="submit" value="更新する">
</form>

create.htmlと似ているが、{{ object.title }}と{{ object.memo }}でviews.pyで取得したレコードを表示している。

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/update/1 にアクセスすると以下によう表示され、更新するとlistが表示されて先程更新したデータが表示される。
しかしながら、『update/1』のURL Pathに/<int:pk>がに当たる数字を画面上表示していないため、わかりにくいので、 listページからそれぞれのレコードに対してリンクを貼ることにする。

Listページからのリンクを作成

templates/list.html

<h1>list Page</h1>
<ul>
    {% for item in object_list %}
        <li>
            {{ item.title }}: {{ item.memo }}
            <a href="{% url 'update' item.pk %}">編集する</a>
        </li>
    {% endfor %}
</ul>

aタグを使い、リンクを作成する。
ここでもurl指定はsamplecrud/urls.pyのnameに指定した名前を利用できる。
また、どのレコードか一意に特定するため、『item.pk』でパラメータをしていることがポイントとなる。

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/list/ を表示すると以下の用になり、クリックするとupdate画面に移動できる。

f:id:n-guitar:20201228214818p:plain:w300

Dalete画面の作成

最後にDBのレコードを削除する画面を作成する。
以下のファイルをそれぞれ編集する。

samplecrud/urls.py

from django.urls import path
from .views import list_page, create_page, update_page, delete_page

urlpatterns = [
    path('list/', list_page, name='list'),
    path('create/', create_page, name='create'),
    path('update/<int:pk>', update_page, name='update'),
    path('delete/<int:pk>', delete_page, name='delete'),
]

updateと同じく、どのレコードかを特定する/<int:pk>を指定する。

samplecrud/views.py

from django.shortcuts import render, redirect
from .models import Todo

def list_page(request):
    object_list = Todo.objects.all()
    return render(request, 'list.html', {'object_list':object_list})

def create_page(request):
    if request.method == 'POST':
        title = request.POST['title']
        memo = request.POST['memo']
        Todo.objects.create(title=title, memo=memo)
        return redirect('list')
    return render(request, 'create.html')

def update_page(request, pk):
    if request.method == 'POST':
        title = request.POST['title']
        memo = request.POST['memo']
        Todo.objects.filter(pk=pk).update(title=title, memo=memo)
        return redirect('list')
    object = Todo.objects.get(pk=pk)
    return render(request, 'update.html', {'object':object})

def delete_page(request, pk):
    if request.method == 'POST':
        object = Todo.objects.get(pk=pk)
        object.delete()
        return redirect('list')
    object = Todo.objects.get(pk=pk)
    return render(request, 'delete.html', {'object':object})

deleteの場合は一度削除してよいかもう一度確認する画面を表示する。
object = Todo.objects.get(pk=pk) で特定のレコードを取得し、 object.delete() でレコードを削除する。

templates/delete.html

<h1>Delete Page</h1>

<form action="" method="POST"> {% csrf_token %}
    <p>
        {{ object.title }}{{ object.memo }}
        を本当に削除しますか?
    </p>
    <input type="submit" value="削除する">
</form>

listからdeleteへのリンクもupdate同様作成しておく

<h1>list Page</h1>
<ul>
    {% for item in object_list %}
        <li>
            {{ item.title }}: {{ item.memo }}
            <a href="{% url 'update' item.pk %}">編集する</a>
            <a href="{% url 'delete' item.pk %}">削除する</a>
        </li>
    {% endfor %}
</ul>

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/list/ を表示すると以下の用になり、削除をクリックするとレコードが削除される。

f:id:n-guitar:20201228215828p:plain:w300
f:id:n-guitar:20201228220915p:plain:w300
f:id:n-guitar:20201228220933p:plain:w300

以上、Djangoのgeneric viewを利用してCRUDを行う方法でした。

/dev/randomと/dev/urandomの違い

本記事で行うこと

  • /dev/randomと/dev/urandomの違いについて簡単に試す

モチベーション

  • /dev/randomを利用している本番処理があったが、ブロック待ちが発生してしまい障害になった。
  • /dev/randomを推奨されておらず、/dev/urandomを利用することが推奨されていることを知ったため、なぜ何かを確認する。

環境

  • CentOS7

調査

早速wikiの参照になるが、
/dev/random - Wikipedia

/dev/randomは真の乱数を発生させるためにデバイス入力とかのノイズを利用するけど、 エントロピープールにノイズが内場合、ブロックする仕様なんですね。なのでノイズ収集するまで待たされる。
その一方、/dev/urandomはブロック待ちがない。
というのを理解した。

真の乱数を発生させるためには、"機械的"ではだめで、外的要因が必要とのこと。
・/dev/randomは外的要因=デバイス入力 を要求し、
・/dev/urandomは機械的に乱数を計算する。
(同じことを2回言った気がするが気にしないこととする)

実際の世界では、
オンプレミスでも、クラウド利用でも基本的にVMを利用することが多く、今回の障害が発生したシステムはオンプレミスでリモート越しにアクセスする。
リモート越しにアクセスすると言うことは特にデバイス入力とかあまり発生しないイメージなので、ブロックが頻繁に起きそうじゃないか? と考え、/dev/randomの動作を手元のCentOS7で試してみると以下の結果となった。

mac上からterminalでssh → CentOS7(VMFusion) mac上のssh越しだと、新しく乱数を全然生成しない。
以下は意味不明な文字列だが乱数と理解してほしい。
この状態では以下の状態で固まった状態に見える。

# cat /dev/random
�^\
   �i��Z  >?���ݚ�K�M�E�ڛ�Mk�

・CentOS7(VMFusion)のConsoleでマウスを動かすと、新たに乱数を生成する。
上記の乱数から新たに乱数が増えていることがわかる。

# cat /dev/random
�^\
   �i��Z  >?���ݚ�K�M�E�ڛ�Mk���ES�8�g���>=z�rPomD:��:;
                                                     �8؆+

つまり我々が普段利用しているデータセンターにあるVMに対しては基本リモートで接続するため
/dev/randomを利用すると、ほぼブロックしてしまう。 誰かがデータセンタに駆けつけてコンソールアクセスしないと、ノイズ発生しないのでエントロピープールが空状態ということ。
真の乱数を使うことなんでないはずなので基本dev/randomは使わないほうが良さそう。

【Django3.1】generic viewを利用してCRUDを行う方法

f:id:n-guitar:20210111031234p:plain:w800

本記事で行うこと

  • DjangoCRUD(Create/Read/Update/Delete)をgeneric viewを利用して行う。
  • Pythonはpyenv環境を利用

なお本記事のソースコードgithub上に公開しています。

GitHub - n-guitar/django-samples at feature/first/sample.generic-view

モチベーション

  • Djangoを教える時のサンプルとして利用したいため。

環境

$ sw_vers
ProductName:    macOS
ProductVersion: 11.1
BuildVersion:   20C69
$ python -V
Python 3.8.0

事前準備 pyenv環境

$ python -m venv env
$ source env/bin/activate
$ export PATH=$PWD/env/bin:$PATH

Djangoのインストール

$ pip install django==3.1.3

Django 初期設定

  • プロジェクトとアプリを作成
  • プロジェクトでtempletsフォルダを設定
$ django-admin startproject confug .
$ django-admin startapp samplecrud
$ mkdir templates
  • settings.py編集 appの追加とtemplateフォルダを参照するように編集

confug/settings.py

・・・
# samplecrudを追加
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'samplecrud', # 追加
]
・・・
# TEMPLATESのDIRSを変更
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'], # 変更
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

動作確認

$ python manage.py runserver

http://127.0.0.1:8000/ にアクセスして以下が表示されればOK f:id:n-guitar:20201228201112p:plain:w600
以上で準備完了。
一旦control + cで止めておく。

現時点で以下のようなディレクトリ構成になっている。
f:id:n-guitar:20201228201643p:plain:w300

CRUD用のDBを作成

今回は以下のようなDBを作成する

Column Field
title CharField(max_length=100)
memo TextField(max_length=255)

samplecrud/models.py

from django.db import models

class Todo(models.Model):
    title = models.CharField(max_length=100)
    memo = models.TextField(max_length=255)

    def __str__(self):
            return self.title

DBの反映

$ python manage.py makemigrations samplecrud
Migrations for 'samplecrud':
  samplecrud/migrations/0001_initial.py
    - Create model Todo

$ python manage.py migrate samplecrud 
Operations to perform:
  Apply all migrations: samplecrud
Running migrations:
  Applying samplecrud.0001_initial... OK

Read画面の作成

早速Read画面を作成するがその前に、URLのつなぎこみを行う。 ※直接configからURLを指定してもいいが、通常アプリごとに作成する多い。

URLのつなぎこみ

$ touch samplecrud/urls.py

現時点のディレクトリ構造は以下の通りになっている。 f:id:n-guitar:20201228202723p:plain:w300

URLのつなぎこみ。
config/urls.pyを編集する

from django.contrib import admin
from django.urls import path, include # include追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('samplecrud/', include('samplecrud.urls')), # 追加
]

上記のURLは『http://127.0.0.1:8000/samplecrud/』でアクセスした時、『samplecrud/urls.py』記載されているURLにルーティングするという意味。

samplecrud/urls.pyを編集する
※現時点ではListPageはviewsの中に存在していないため、エラーになるがこの次に作成する。

from django.urls import path
from .views import ListPage

urlpatterns = [
    path('list/', ListPage.as_view(), name='list'),
]

Read画面の作成

いよいよ画面を作成していくが CRUDを実現するのにdjango.viewsのgeneric viewと呼ばれるもの利用する。 マニュアルはあるが、ソースコードを見たほうが早い場合があるので必要に応じて参照する。

generic.ListViewを継承したクラスを作り、どのDBのTableを利用して、どのhtmlファイルにレンダリングするかを以下のように書くだけ。
samplecrud/views.py

from django.shortcuts import render
from .models import Todo
from django.views import generic

class ListPage(generic.ListView):
    model = Todo
    template_name = 'list.html'

レンダリング先のhtmlがないので作成する。
templates/list.htmlの作成

<h1>list Page</h1>
<ul>
    {% for item in object_list %}
        <li>{{ item.title }}: {{ item.memo }}</li>
    {% endfor %}
</ul>

Todo tableから取得した値がobject_listというオブジェクトに格納され、list.htmlに渡される

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/list/ にアクセスする

以下のように表示されればOK。
今はテーブルに何もレコードがないため、何も表示されない。 f:id:n-guitar:20201228210609p:plain:w300

Create画面の作成

続けてDBのレコードを追加する画面を作成する。
以下のファイルをそれぞれ編集する。

samplecrud/urls.py

from django.urls import path
from .views import ListPage, CreatePage # 追記

samplecrud/urls.py
urlpatterns = [
    path('list/', ListPage.as_view(), name='list'),
    path('create/', CreatePage.as_view(), name='create'),  # 追記
]

こちらは先程のlistと同じ

samplecrud/views.py

from django.shortcuts import render
from .models import Todo
from django.views import generic
from django.urls import reverse_lazy

class ListPage(generic.ListView):
    model = Todo
    template_name = 'list.html'

class CreatePage(generic.CreateView):
    model = Todo
    template_name = 'create.html'
    fields = ('title','memo')
    success_url = reverse_lazy('list')

createのときはどのFieldを更新するか『fields』で指定する。
『success_url = reverse_lazy('list')』はcreateが成功したときにどの画面を表示するかするかを示す。
ちはみに『reverse_lazy('list')』のlistはsamplecrud/urls.pyのnameに指定した名前を利用できる。

templates/create.html

<h1>Create Page</h1>

<form action="" method="POST"> {% csrf_token %}
    {{ form.as_p}}
    <input type="submit" value="作成する">
</form>

Djangoは『 {% csrf_token %}』をつけることでデフォルトでCSRFの検証を行ってくれる。(逆につけないとエラーになる)
『{{ form.as_p}}』は fieldsで指定した項目を

タグで展開してくれる。(余談だがCreateViewがForm classを継承しているため利用できる)

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/create/ にアクセスすると以下によう表示され、更新するとlistが表示されて先程更新したデータが表示される。
何件か登録してみるとすべて表示されることがわかる。

f:id:n-guitar:20201228212434p:plain:w300
f:id:n-guitar:20201228212920p:plain:w300

Update画面の作成

続けてDBのレコードを更新する画面を作成する。
以下のファイルをそれぞれ編集する。

samplecrud/urls.py

from django.urls import path
from .views import ListPage, CreatePage, UpdatePage # 追記

urlpatterns = [
    path('list/', ListPage.as_view(), name='list'),
    path('create/', CreatePage.as_view(), name='create'),
    path('update/<int:pk>', UpdatePage.as_view(), name='update'), # 追記
]

こちらは先程と若干異なり、URL Pathに/<int:pk>が記載されている。
これはDBのどのレコードを更新していいか判断できないので<int:pk>で一意にレコードを特定する。
DjangoではデフォルトでTableを作成するとidというPrimary KeyになるColumnが作成される。

samplecrud/views.py

from django.shortcuts import render
from .models import Todo
from django.views import generic
from django.urls import reverse_lazy

class ListPage(generic.ListView):
    model = Todo
    template_name = 'list.html'

class CreatePage(generic.CreateView):
    model = Todo
    template_name = 'create.html'
    fields = ('title','memo')
    success_url = reverse_lazy('list')

class UpdatePage(generic.UpdateView):
    model = Todo
    template_name = 'update.html'
    fields = ('title','memo')
    success_url = reverse_lazy('list')

createのとき同じでFieldを更新するか『fields』で指定する。

templates/update.html

<h1>Update Page</h1>

<form action="" method="POST"> {% csrf_token %}
    {{ form.as_p}}
    <input type="submit" value="更新する">
</form>

create.htmlと全く同じで問題ない。(valueの値だけ更新するに変更した。)

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/update/1 にアクセスすると以下によう表示され、更新するとlistが表示されて先程更新したデータが表示される。
しかしながら、『update/1』のURL Pathに/<int:pk>がに当たる数字を画面上表示していないため、わかりにくいので、 listページからそれぞれのレコードに対してリンクを貼ることにする。

Listページからのリンクを作成

templates/list.html

<h1>list Page</h1>
<ul>
    {% for item in object_list %}
        <li>
            {{ item.title }}: {{ item.memo }}
            <a href="{% url 'update' item.pk %}">編集する</a>
        </li>
    {% endfor %}
</ul>

aタグを使い、リンクを作成する。
ここでもurl指定はsamplecrud/urls.pyのnameに指定した名前を利用できる。
また、どのレコードか一意に特定するため、『item.pk』でパラメータをしていることがポイントとなる。

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/list/ を表示すると以下の用になり、クリックするとupdate画面に移動できる。

f:id:n-guitar:20201228214818p:plain:w300

Dalete画面の作成

最後にDBのレコードを削除する画面を作成する。
以下のファイルをそれぞれ編集する。

samplecrud/urls.py

from django.urls import path
from .views import ListPage, CreatePage, UpdatePage, DeletePage # 追記

urlpatterns = [
    path('list/', ListPage.as_view(), name='list'),
    path('create/', CreatePage.as_view(), name='create'),
    path('update/<int:pk>', UpdatePage.as_view(), name='update'),
    path('delete/<int:pk>', DeletePage.as_view(), name='delete'), # 追記
]

updateと同じく、どのレコードかを特定する/<int:pk>を指定する。

samplecrud/views.py

from django.shortcuts import render
from .models import Todo
from django.views import generic
from django.urls import reverse_lazy

class ListPage(generic.ListView):
    model = Todo
    template_name = 'list.html'

class CreatePage(generic.CreateView):
    model = Todo
    template_name = 'create.html'
    fields = ('title','memo')
    success_url = reverse_lazy('list')

class UpdatePage(generic.UpdateView):
    model = Todo
    template_name = 'update.html'
    fields = ('title','memo')
    success_url = reverse_lazy('list')

class DeletePage(generic.DeleteView):
    model = Todo
    template_name = 'delete.html'
    success_url = reverse_lazy('list')

deleteの場合は一度削除してよいかもう一度確認する画面を表示する。 ※余談だが、DeleteViewは今回登場させていない1レコードの詳細表示されるDetailViewを継承しているので、DetailViewを兼ねることもできる。
(Detail/Deleteでそれぞれ複数の同じページを作成させたくない時や、Deleteの確認をポップアップ表示させたい時などにDeleteViewをDetailViewを兼ねて自分は利用する)

templates/delete.html

<h1>Delete Page</h1>

<form action="" method="POST"> {% csrf_token %}
    <p>
        {{ object.title }}{{ object.memo }}
        を本当に削除しますか?
    </p>
    <input type="submit" value="削除する">
</form>

listからdeleteへのリンクもupdate同様作成しておく

<h1>list Page</h1>
<ul>
    {% for item in object_list %}
        <li>
            {{ item.title }}: {{ item.memo }}
            <a href="{% url 'update' item.pk %}">編集する</a>
            <a href="{% url 'delete' item.pk %}">削除する</a>
        </li>
    {% endfor %}
</ul>

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/list/ を表示すると以下の用になり、削除をクリックするとレコードが削除される。

f:id:n-guitar:20201228215828p:plain:w300
f:id:n-guitar:20201228220915p:plain:w300
f:id:n-guitar:20201228220933p:plain:w300

以上、Djangoのgeneric viewを利用してCRUDを行う方法でした。

【mac issue】『xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun』エラー

mac issue系

  • 自分のメモようなので雑です。

本記事で行うこと

以下エラーの解消方法 * 『xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun』

再現手順

$ git clone XXXXX
xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun

環境

$ sw_vers
ProductName:    macOS
ProductVersion: 11.1
BuildVersion:   20C69

対応方法

  • Xcodeツールキットをインストールする。
  • 以前にインストールしたことがある場合でも、再登録するか、最新バージョンに更新する。
$ xcode-select --install
xcode-select: note: install requested for command line developer tools

f:id:n-guitar:20201228183229p:plain:w600 f:id:n-guitar:20201228183705p:plain:w600 上記で完了。 再度git コマンドを行うと問題なく動作する。

xterm.jsの見た目を変更する

本記事で行うこと

  • xterm.jsのterminal optionをつかって見た目を変更してみます。
  • xterm.jsのインストールやキーボード入力をする方法は以下を参照ください。 n-guitar.hatenablog.com

参考サイト

初期状態

初期状態は以下の状態です。
new Terminalに色々optionを加えてみます。
f:id:n-guitar:20201114205328p:plain:w600

htmlファイル

<!doctype html>
  <html>
    <head>
      <link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
      <script src="node_modules/xterm/lib/xterm.js"></script>
    </head>
    <body>
      <div id="terminal"></div>
      <script>
        let term = new Terminal({
          // ここに色々設定して見る。
        });
        term.open(document.getElementById('terminal'));

        function runFakeTerminal() {
          if (term._initialized) {
            return;
          }

          term._initialized = true;

          term.prompt = () => {
            term.write('\r\n$ ');
          };

          term.writeln('Welcome to xterm.js');
          term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
          term.writeln('Type some keys and commands to play around.');
          term.writeln('');
          term.prompt();

          term.onKey(e => {
            console.log(e)
            const ev = e.domEvent
            const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey

            if (ev.keyCode === 13) {
              term.prompt();
            } else if (ev.keyCode === 8) {
              if (term._core.buffer.x > 2) {
                term.write('\b \b');
              }
            } else if (printable) {
              term.write(e.key);
            }
          });
        }
        runFakeTerminal()

      </script>
    </body>
  </html>
  • terminalの行数を指定します。
let term = new Terminal({
          // ここに色々設定して見る。
          rows: 30, // terminalの行数
        });

少し大きくなりました。
f:id:n-guitar:20201114205419p:plain:w600

  • カーソルを点滅させます。
let term = new Terminal({
          // ここに色々設定して見る。
          rows: 30, // terminalの行数
          cursorBlink: true, //カーソルの点滅
        });

カーソルが点滅するようになりました。
f:id:n-guitar:20201114205801g:plain:w600

  • カーソルをアンダーラインにします。
let term = new Terminal({
          // ここに色々設定して見る。
          rows: 30, // terminalの行数
          cursorBlink: true, //カーソルの点滅
          cursorStyle: 'underline' //カーソルをアンダーライン
        });

f:id:n-guitar:20201114210054p:plain

let term = new Terminal({
          // ここに色々設定して見る。
          rows: 30, // terminalの行数
          cursorBlink: true, //カーソルの点滅
          cursorStyle: 'underline', //カーソルをアンダーライン
          RendererType: 'canvas', //  renderer typeをcanvas
        });
        let term = new Terminal({
          // ここに色々設定して見る。
          rows: 30, // terminalの行数
          cursorBlink: true, //カーソルの点滅
          cursorStyle: 'underline', //カーソルをアンダーライン
          RendererType: 'canvas', //  renderer typeをcanvas
          theme: {
            background: 'darkslategray'
          }
        });

f:id:n-guitar:20201114211713p:plain:w600

  • 文字の色を変える。
        let term = new Terminal({
          // ここに色々設定して見る。
          rows: 30, // terminalの行数
          cursorBlink: true, //カーソルの点滅
          cursorStyle: 'underline', //カーソルをアンダーライン
          RendererType: 'canvas', //  renderer typeをcanvas
          theme: {
            background: 'darkslategray',
            foreground: 'orange',
          }
        });

f:id:n-guitar:20201114212137p:plain:w600

結構自分好みにカスタマイズできそうですね。

xterm.jsでキーボード入力を受け付ける方法

本記事で行うこと

  • xterm.jsを利用して、キーボード入力を受け付けるhtmlを作成する。
  • ※環境はnode.jsを使いますが、最後にCDNサイトを使ってhtmlファイルのみで行う方法も記載しておきます。

モチベーション

  • vscodeのterminalなどで採用されているxterm.jsをつかってプロダクトを作ろうと思ったため。
  • xterm.jsの使い方を開発メンバーと共有したかったため。

環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H15

$ node -v
v12.18.3

xterm.jsのインストール

versionは本記事を書いたときの最新版を利用します。

$ npm install --save xterm@4.9.0

Hello xterm.js

まず公式ページにあるとおり、html上にterminal windowを表示してみます。
github.com

<!doctype html>
  <html>
    <head>
      <link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
      <script src="node_modules/xterm/lib/xterm.js"></script>
    </head>
    <body>
      <div id="terminal"></div>
      <script>
        let term = new Terminal();
        term.open(document.getElementById('terminal'));
        term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
      </script>
    </body>
  </html>

headのpathはインストールした場所を指定してください。
作成したhtmlファイルにブラウザでアクセスすると以下のようにterminalを表示できます。
f:id:n-guitar:20201114190900p:plain

  • term.openでterminalを開いて、term.writeでterminalに文字を書き込めるようです。

この状態だとterminalに向かってキーボードを打っても何も反応しないのですが、
せっかくなのでこの状態で、Chromeの開発者モードで少し遊んで見たいと思います。

Chromeの開発者モードのConsoleを開き以下のように打ち込んで見ると画面が更新されました。

f:id:n-guitar:20201114191438p:plain:w600

term.write("書き込んでみた")

f:id:n-guitar:20201114191719p:plain:w600

Consoleにtermを打ち込むと何やら使えそうな関数が色々出てくるので、公式サイトで使い方を見てみます。

term

f:id:n-guitar:20201114192211p:plain:w600

xtermjs.org Doc -> Class: Terminalを参照しいくつか試した結果を書いておきます。

  • 書き込んで改行する。
term.writeln("改行する")
  • 書き込んでpromptを表示する。 今後Enterを押した時に必ずこれを実行すれば良さそうです。
term.write('\r\n$ ')
  • terminalに書き込まれたものをresetする。 clearが入力された時にこれを実行すれば良さそうな気がします。
term.reset()

キーボード入力を受け付けるhtmlを作成する。

公式が的供しているdemoサイトを参考に以下を作りました。 xterm.js/client.ts at master · xtermjs/xterm.js · GitHub

<!doctype html>
  <html>
    <head>
      <link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
      <script src="node_modules/xterm/lib/xterm.js"></script>
    </head>
    <body>
      <div id="terminal"></div>
      <script>
        let term = new Terminal();
        term.open(document.getElementById('terminal'));
        function runFakeTerminal() {
          if (term._initialized) {
            return;
          }
          term._initialized = true;
          term.prompt = () => {
            term.write('\r\n$ ');
          };

          term.writeln('Welcome to xterm.js');
          term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
          term.writeln('Type some keys and commands to play around.');
          term.writeln('');
          term.prompt();

          term.onKey(e => {
            console.log(e)
            const ev = e.domEvent
            const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey

            if (ev.keyCode === 13) {
              term.prompt();
            } else if (ev.keyCode === 8) {
              if (term._core.buffer.x > 2) {
                term.write('\b \b');
              }
            } else if (printable) {
              term.write(e.key);
            }
          });
        }
        runFakeTerminal()
      </script>
    </body>
  </html>

これでキーボード入力を受け付けるようになりました。
f:id:n-guitar:20201114201055p:plain:w600

少しだけ解説すると・・・

  • term.prompt関数は呼ばれるとterm.write('\r\n$ ')を実行しバッファ2の『$ 』を書き込みます。
  • keyCode === 13はEnter
  • keyCode === 8はBackspace
  • term._core.buffer.x > 2はtermのバッファが3以上の時1文字消します。
  • printable = !ev.altKey && !ev.ctrlKey && !ev.metaKeyはこの条件がtrueの時の判定に利用
  • else if (printable) {term.write(e.key);でprintableがtrueの時にterm.write(e.key)して入力をterminal上に書き込みます。

キーボード入力とコードの対応表 KeyboardEvent.keyCode - Web API | MDN

CDNサイト版

手っ取り早く試してみたい人は以下をhtmlファイルで保存すればnode.jsなしでも実行できます。
CDNサイトがある限り。

<!doctype html>
  <html>
    <head>
      <link rel="stylesheet" href="https://unpkg.com/xterm@4.9.0/css/xterm.css">
      <script src="https://unpkg.com/xterm@4.9.0/lib/xterm.js"></script>
    </head>
    <body>
      <div id="terminal"></div>
      <script>
        let term = new Terminal();
        term.open(document.getElementById('terminal'));
        function runFakeTerminal() {
          if (term._initialized) {
            return;
          }
          term._initialized = true;
          term.prompt = () => {
            term.write('\r\n$ ');
          };

          term.writeln('Welcome to xterm.js');
          term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
          term.writeln('Type some keys and commands to play around.');
          term.writeln('');
          term.prompt();

          term.onKey(e => {
            console.log(e)
            const ev = e.domEvent
            const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey

            if (ev.keyCode === 13) {
              term.prompt();
            } else if (ev.keyCode === 8) {
              if (term._core.buffer.x > 2) {
                term.write('\b \b');
              }
            } else if (printable) {
              term.write(e.key);
            }
          });
        }
        runFakeTerminal()
      </script>
    </body>
  </html>

alpine linuxをVMWareFusionにインストールする

本記事で行うこと

  • alpine linuxをVMWareFusionにインストールする。
  • 管理用のuserを作成する。
  • macのterminalからsshで管理用のuserにログインする。

モチベーション

  • Linux上で動作するapplicationの実行環境として軽量なLinuxを探していた。

環境

macbook pro上でVMwareFusionを利用する

  • macのversion
$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H15

alpine linuxのisoファイルのダウンロード

以下のサイトからalpine linux(virtual版)のisoファイルをダウンロードします。
alpinelinux.org
ページ下に『古いリリースはここにあります。』とあるのでそこからisoファイルを選んでダウンロードします。
Index of /alpine/v3.12/releases/x86_64/

この時最新だった『alpine-virt-3.12.1-x86_64.iso』を選択します。
isoファイルで提供されているもので最も最小のvirtual版を使います。
たった40MB程度しかなく、よく利用しているCentOS7のminimal版でも7.9は約1GBもあるので、この時点で驚きです。
f:id:n-guitar:20201114162846p:plain:w600

VMWareFusionにインストール

早速VMWareFusionしてみます。

f:id:n-guitar:20201113225908p:plain:w600 f:id:n-guitar:20201113225922p:plain:w600 f:id:n-guitar:20201113225933p:plain:w600 あとでapline上でk3sを動かす予定でしたので、少し大きめの2CPU/2MBに変更します。
f:id:n-guitar:20201114163309p:plain:w600

恐ろしく早く起動してきました。10秒もかかってないです。 f:id:n-guitar:20201113230253p:plain:w600

rootでログインし、setup-apine scriptが用意されているので実行して各設定おwしていきます。 f:id:n-guitar:20201113230418p:plain:w600

setup-apine の実行
ここで『setup- + TAB』を押すと候補が表示されます。
各項目を別々に設定することも可能です。
f:id:n-guitar:20201114163907p:plain:w600
改めてsetup-apine を実行
f:id:n-guitar:20201114164122p:plain:w300

キーボードの設定で『us』と『mac-us』を選択。
f:id:n-guitar:20201113230621p:plain:w600
f:id:n-guitar:20201113230711p:plain:w600

ホスト名はdefaultが『localhost』なのでそのままEnterを押します。 f:id:n-guitar:20201114164348p:plain:w600

VMWareFusionはdefaultでdhcpでipを割り振ってくれるので、全てそのままEnterを押します。
自動的に設定されました。 f:id:n-guitar:20201114164647p:plain:w600

rootのpasswordを設定します。
f:id:n-guitar:20201114164816p:plain:w300

タイムゾーンを『Asia/Tokyo』に設定します。
f:id:n-guitar:20201114164923p:plain:w600

Proxyは利用しないのでそのままEnterを押します。
f:id:n-guitar:20201114165033p:plain:w600

NTPはchronyを利用したいのでそのままEnterを押します。
f:id:n-guitar:20201114165120p:plain:w600

package repositoryは速いサイトを検索して設定してくれるfを入れてEnterを押します。
f:id:n-guitar:20201114171743p:plain:w600

SSHはdefaultのopensshを使いたいのでそのままEnterを押します。
f:id:n-guitar:20201114165504p:plain:w600

diskはsdaを入れてEnterを押します。
f:id:n-guitar:20201114165603p:plain hddにインストールしてLinuxを使いたいのでsysモードにします。
f:id:n-guitar:20201114165741p:plain:w600 yを入れて、Enterでフォーマットします。
f:id:n-guitar:20201114172058p:plain:w600

しばらくして完了したら、rebootして設定完了です。
f:id:n-guitar:20201114172410p:plain:w300

reboot後も10秒かからずに起動してきました。
今後はmacのterminalからsshして作業したいので、管理用のuserを作成します。
f:id:n-guitar:20201114172633p:plain:w300

管理用のuserの作成

管理用のuserを作成します。
f:id:n-guitar:20201114180445p:plain:w600

管理用のuserでsudoで管理用のコマンドを許可したいのですが、
標準のaplineにはsudoがdefaultでは入っていません。
f:id:n-guitar:20201114173819p:plain:w300

apline公式でもユーザに管理アクセスさせたいときはsudoを入れて、wheelに入れてとありました。
docs.alpinelinux.org

ちなみに本内容とは脱線しますが、wheelというのは伝統的にユーザに管理権限を設定するためのグループのようです。
CentOS系でメディアインストールする時に管理者ユーザを作成すると自動的にwheelに入るので実は慣れ親しみがあります。
ホイール (コンピュータ) - Wikipedia

それではsudoをインストールして管理用のユーザをwheelに所属させます。

それではsudoをインストール
(なんと94MiBもありました・・・)
f:id:n-guitar:20201114175849p:plain:w300

管理用のユーザをwheelに所属させます。
f:id:n-guitar:20201114180808p:plain:w300

wheelグループにsudo権限を追加します。
f:id:n-guitar:20201114183324p:plain:w600

macのterminalからsshで管理用のuser

この状態でmacからログインしてみる。

管理者でないとアクセスできないファイルにsudoでアクセスすることができました。

$ ssh adminuser@192.168.128.27
adminuser@192.168.128.27's password:
Welcome to Alpine!

The Alpine Wiki contains a large amount of how-to guides and general
information about administrating Alpine systems.
See <http://wiki.alpinelinux.org/>.

You can setup the system with the command: setup-alpine

You may change this message by editing /etc/motd.

localhost:~$

$ ls -l /etc/sudoers.d
ls: can't open '/etc/sudoers.d': Permission denied
total 0

$ sudo ls -l /etc/sudoers.d
[sudo] password for adminuser:
total 4
-rw-r--r--    1 root     root            21 Nov 14 18:33 wheel