Engineer's Memorandum

某メーカー勤務エンジニアの備忘録です。書籍・投資・プログラミングなどについて記載します。

DjangoでのDB削除機能の追加

WebアプリフレームワークDjangoの書籍1を勉強中です。 書籍の1章で説明されているコードSnippetの共有アプリをベースに機能を追加して勉強を継続しています。

今回、DBに登録されたSnippetを削除する機能を追加したので、その内容をメモしています。

DB削除機能の実装例

機能追加前

Snippet削除機能は、Snippet詳細画面に追加しました。 青色の編集ボタンの右にボタンを追加します。

delete機能追加前のSnippet詳細画面

機能追加後

Snippet詳細画面に、赤色の削除ボタンを追加した結果です。

delete機能追加後のSnippet詳細画面

削除ボタンを押すと、確認画面に遷移するように実装しました。

Snippet削除の確認画面

ユーザが異なる場合の挙動

Snippetを登録したユーザ以外は、編集も削除もできないようにしています。 ユーザadminでログインしたとき、ユーザtestuserが登録したSnippetは編集・削除ボタンを表示させません。

編集・削除権限のないユーザのSnippet詳細画面

また、悪意あるユーザがURLを直接入力して編集・削除画面に遷移できないようにしています。 URLが正しくとも、ログインユーザのIDをチェックして編集・削除権限が無ければ404エラーを出すようにしました。

編集・削除権限のないユーザが編集・削除用URLを入力したときのエラー

ファイル構成

migrationディレクトリなど、機能追加に関係のないディレクトリやファイルは省略しています。

$ tree
.
├── __init__.py
├── admin.py
├── apps.py
├── forms.py
├── models.py
├── static
│   └── snippets
│       └── css
│           └── style.css
├── templates
│   └── snippets
│       ├── snippet_delete.html # ファイル追加
│       ├── snippet_detail.html
│       ├── snippet_edit.html # ファイル変更
│       ├── snippet_new.html
│       └── top.html
├── urls.py # ファイル変更
└── views.py # ファイル変更

ソースコード

Djangoのクラスベースview機能で提供されているDeleteViewクラスを利用して実装しました。

views.py

DeleteViewに加えて、ログインしていないユーザをログイン画面に遷移させるため、LoginRequiredMixinも継承しています。 get_querysetメソッドをオーバーライドすることで、作成したユーザのIDとログインユーザIDが一致する場合のみ編集・削除ボタンを表示するためのデータを取得するようにしています。

クラス変数success_urlは正常に処理が行われた場合の遷移先を表し、ここではtopページに遷移させています (reverse_lazyはきちんと理解できていないので、後日redirectやreverseとの違いを追記します)。 pk_url_kwargはurl.pyで使用するURLのPrimary Keyを、snippet_idという表記で上書きしています(デフォルトはpk)2

from django.views.generic import DeleteView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
class SnippetDeleteView(LoginRequiredMixin, DeleteView):
template_name = "snippets/snippet_delete.html"
model = Snippet
success_url = reverse_lazy("top")
pk_url_kwarg = "snippet_id"
def get_queryset(self):
queryset = super().get_queryset()
filtered_queryset = queryset.filter(created_by_id=self.request.user.id)
return filtered_queryset

urls.py

urlpatternsに以下の削除用ページ情報を追加します。 int:snippet_idとなっているのは、SnippetDeleteViewのクラス変数pk_url_kwarg を上書きしたためです。

urlpatterns = [
...
path('<int:snippet_id>/delete/', views.SnippetDeleteView.as_view(), name="snippet_delete"),
...
]

HTML template

CSSは、bootstrap(チートシートサイト)を利用しています。

snippet_detail.html

編集ボタンの次に、削除ボタンを追加します。 必要な情報を削除するリスクがあるので、注意喚起のため赤色ボタンにしました。

<div class="snippet-date">
投稿日:{{ snippet.created_at|date:"DATETIME_FORMAT" }}
{% if user.is_authenticated and snippet.created_by_id == user.id %}
<a class="btn btn-primary" href="{% url 'snippet_edit' snippet.id %}">編集</a>
<!-- 以下の行を追加 -->
<a class="btn btn-danger" href="{% url 'snippet_delete' snippet.id %}">削除</a>
{% endif %}
</div>

snippet_delete.html (追加ファイル)

urls.pyに追加した遷移先を記述するため、新たに追加したファイルです。 最低限の動作としては、確定ボタンだけでも良いのですが、 ユーザが再確認できるよう、Snippetの詳細ページを踏襲した情報も載せています。

{% extends "base.html" %}
{% load pygmentize %}
{% load django_bootstrap5 %}
{% block extraheader %}
<style>{% pygments_css %}</style>
{% endblock %}
{% block main %}
<form method="POST">
{% csrf_token %}
<h3>"{{ snippet.title }}"を本当に削除しますか?</h3>
<div class="source-code">
{{ snippet.code|pygmentize:"python3" }}
</div>
<p>{{ snippet.description | urlize }}</p>
<button type="submit" class="btn btn-danger">確定</button>
</form>
{% endblock %}

書籍

まず実際にWebアプリを作り、その後詳細な機能を解説する形式で、Djangoを学ぶことができます。


  1. 芝田 将 "実践Django Pythonによる本格Webアプリケーション開発"
  2. Classy Class-Based Views: DeleteView