Djangoで作る家計簿アプリ ④登録、編集、削除ページの実装

Djangoで作る家計簿アプリシリーズの四つ目の記事です。
今回はざっくりと支出と収入に対しての登録、編集、削除機能を追記していきます。

登録機能


まず作成用のURLパターンを追記します。

# kakeibo/urls.py
from django.urls import path
from . import views

app_name = 'kakeibo'

urlpatterns = [
    path('', views.PaymentList.as_view(), name='payment_list'),
    path('income_list/', views.IncomeList.as_view(), name='income_list'),
    # 追加
    path('payment_create/', views.PaymentCreate.as_view(), name='payment_create'),
    path('income_create/', views.IncomeCreate.as_view(), name='income_create'),
]


次に作成用のフォームを作ります。

#kakeibo/forms.py
from django import forms
from .models import PaymentCategory, Payment, Income  # 追加
from django.utils import timezone
from .widgets import CustomRadioSelect

...

class PaymentCreateForm(forms.ModelForm):
    """支出登録フォーム"""

    class Meta:
        model = Payment
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form'
            field.widget.attrs['placeholder'] = field.label
            field.widget.attrs['autocomplete'] = 'off'


class IncomeCreateForm(forms.ModelForm):
    """収入登録フォーム"""

    class Meta:
        model = Income
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form'
            field.widget.attrs['placeholder'] = field.label
            field.widget.attrs['autocomplete'] = 'off'


登録と支出のモデルは構成を同じにしているので、ほぼ同じです。
views.pyも同様です。

#kakeibo/views.py
from django.views import generic
from .models import Payment, PaymentCategory, Income, IncomeCategory
from .forms import PaymentSearchForm, IncomeSearchForm, PaymentCreateForm, IncomeCreateForm  # 追加
from django.urls import reverse_lazy  # 追加

...

class PaymentCreate(generic.CreateView):
    """支出登録"""
    template_name = 'kakeibo/register.html'
    model = Payment
    form_class = PaymentCreateForm

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['page_title'] = '支出登録'
        return context

    def get_success_url(self):
        return reverse_lazy('kakeibo:payment_list')


class IncomeCreate(generic.CreateView):
    """収入登録"""
    template_name = 'kakeibo/register.html'
    model = Income
    form_class = IncomeCreateForm

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['page_title'] = '収入登録'
        return context

    def get_success_url(self):
        return reverse_lazy('kakeibo:income_list')


templateは同じregister.htmlを使いまわすようにして、ページのタイトルだけcontextに乗せて渡すようにしています。

ヘッダーにリンク情報を追記しましょう。

<!-- kakeibo/templates/kakeibo/base.html -->
 <header class="page-header">
    <h1>
      <a href="{% url 'kakeibo:payment_list' %}" class="header-title">家計簿アプリ</a>
    </h1>
    <nav class="nav">
      <ul class="main-nav ml-5">
        <li class="ml-5">
          <a href="{% url 'kakeibo:payment_list'%}">支出一覧</a>
        </li>
        <li class="ml-5">
          <a href="{% url 'kakeibo:income_list'%}">収入一覧</a>
        </li>
        <!-- 追加 -->
        <li class="ml-5">
          <a href="{% url 'kakeibo:payment_create'%}">支出登録</a>
        </li>
        <li class="ml-5">
          <a href="{% url 'kakeibo:income_create'%}">収入登録</a>
        </li>
    </nav>
  </header>


次にregister.htmlを作成します。

<!-- kakeibo/templates/kakeibo/register.html -->
{% extends 'kakeibo/base.html' %}

{% block content %}
<h1>{{ page_title }}</h1>
<form method="POST">
  {% csrf_token %}
  {{ form.non_field_errors }}
  {% for field in form %}
  <div class="mt-4">
    {{ field }}
    {{ field.errors }}
  </div>
  {% endfor %}
  <button class="btn mt-4" type="submit" name="button">送信</button>
</form>
{% endblock %}






このように同じデザインで支出と収入のタイトルのみ切り替わり表示されます。

ためしに登録をしてみます。



送信を押すと一覧に遷移する仕様です。


日付の入力を簡単にする

yyyy-mm-dd形式で入力するのはちょっと大変です。
日付の入力にカレンダー選択機能をつけましょう。
これは簡単で、jQueryのdatepickerを使うだけです。

<!-- kakeibo/templates/kakeibo/register.html -->
{% extends 'kakeibo/base.html' %}
{% block content %}
<h1>{{ page_title }}</h1>
<form method="POST">
 ...
</form>

<!-- date picker -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script type="text/javascript">
  $(function() {
    $('#id_date').datepicker({
      dateFormat: 'yy-mm-dd',
      firstDay: 1,
      dayNamesMin: ["日", "月", "火", "水", "木", "金", "土"],
      monthNames: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
    });
  })
</script>
{% endblock %}


このようにフォーカス時にカレンダーが出現します。


成功メッセージを表示する

現状ですと、登録後に一覧ページにただ遷移するだけです。
登録ができているのかできていないのか、ユーザーからはよくわかりません。
なので、遷移後に簡単な成功メッセージを表示するようにしていきましょう。
今回はdjango.contribのmessagesを使って実装をしていきます。

#kakeibo/views.py
...
from django.contrib import messages  # 追加
from django.shortcuts import redirect  # 追加

...

class PaymentCreate(generic.CreateView):
    """支出登録"""
    template_name = 'kakeibo/register.html'
    model = Payment
    form_class = PaymentCreateForm

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['page_title'] = '支出登録'
        return context

    def get_success_url(self):
        return reverse_lazy('kakeibo:payment_list')

    # 追加
    # バリデーション時にメッセージを保存
    def form_valid(self, form):
        self.object = payment = form.save()
        messages.info(self.request,
                      f'支出を登録しました\n'
                      f'日付:{payment.date}\n'
                      f'カテゴリ:{payment.category}\n'
                      f'金額:{payment.price}円')
        return redirect(self.get_success_url())


このようにすると、メッセージを伴った状態で、遷移することができます。
次にbase.htmlにメッセージがある場合は表示する、という処理を書いていきましょう。

<!-- kakeibo/templates/kakeibo/base.html -->
...
  <div class="layout">
    <div class="container">
      <main>
        <!-- メッセージがある場合は表示する -->
        {% if messages %}
        <div class="alert alert-success js-alert" id="js-alert">
          <button class="alert-btn-close js-alert-close" type="button"></button>
     <!-- メッセージを取り出す -->
          {% for message in messages %}
          <p> {{ message|linebreaks }}</p>
          {% endfor %}
        </div>
        {% endif %}
        {% block content %}{% endblock %}
      </main>
    </div>
  </div>
...
<script type="text/javascript">
  // ×を押して閉じられるようにする
  for (const element of document.querySelectorAll('.js-alert > .js-alert-close')) {
    element.addEventListener('click', e => {
      e.target.parentElement.classList.add('is-hidden');
    });
  }
</script>

</html>


表示して消せないとよくないので、×ボタンを押したら、メッセージエリア自体を消去する処理をJavaScriptで書いています。
is-hiddenクラスを付与して、display:noneのプロパティをあてるという流れです。
次にアラートメッセージのスタイルを追記しましょう。

/* kakeibo/static/kakeibo/css/style.css */
...
/* --------------------------------
 * アラートメッセージ
 * -------------------------------- */
.alert {
  position: relative;
  padding: 1.25rem 1.5rem;
  margin-bottom: 1rem;
  border: 1px solid transparent;
  line-height: 1.3;
}

.alert-btn-close {
  font-size: 2rem;
  color: #888;
  position: absolute;
  top: 0;
  right: 0;
  z-index: 2;
  padding: 1.5625rem 1.5rem;
  border: None;
  background-color: inherit;
  cursor: pointer;
}

.alert-btn-close:hover {
  opacity: 0.5;
}

.alert-success {
  color: #006e2c;
  background-color: #ccf1db;
  border-color: #b3e9c9
}

.is-hidden {
  display: none;
}


支出を登録すると、このようなメッセージが出るようになります。


収入のメッセージも支出と同様にviews,pyに追加するだけです。

#kakeibo/views.py
class IncomeCreate(generic.CreateView):
    ...
    def form_valid(self, form):
        self.object = income = form.save()
        messages.info(self.request,
                      f'収入を登録しました\n'
                      f'日付:{income.date}\n'
                      f'カテゴリ:{income.category}\n'
                      f'金額:{income.price}円')
        return redirect(self.get_success_url())


編集、削除機能を加える


次に一覧ページから編集と削除を行えるようにしていきましょう。

#kakeibo/urls.py

urlpatterns = [
   .... 
   # 追加
    path('payment_update/<int:pk>/', views.PaymentUpdate.as_view(), name='payment_update'),
    path('income_update/<int:pk>/', views.IncomeUpdate.as_view(), name='income_update'),
    path('payment_delete/<int:pk>/', views.PaymentDelete.as_view(), name='payment_delete'),
    path('income_delete/<int:pk>/', views.IncomeDelete.as_view(), name='income_delete'),
]


viewも追加します。
それぞれの画面で成功メッセージも設定しておきましょう。

#kakeibo/views.py

...
class PaymentUpdate(generic.UpdateView):
    """支出更新"""
    template_name = 'kakeibo/register.html'
    model = Payment
    form_class = PaymentCreateForm

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['page_title'] = '支出更新'
        return context

    def get_success_url(self):
        return reverse_lazy('kakeibo:payment_list')

    def form_valid(self, form):
        self.object = payment = form.save()
        messages.info(self.request,
                      f'支出を更新しました\n'
                      f'日付:{payment.date}\n'
                      f'カテゴリ:{payment.category}\n'
                      f'金額:{payment.price}円')
        return redirect(self.get_success_url())


class IncomeUpdate(generic.UpdateView):
    """収入更新"""
    template_name = 'kakeibo/register.html'
    model = Income
    form_class = IncomeCreateForm

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['page_title'] = '収入更新'
        return context

    def get_success_url(self):
        return reverse_lazy('kakeibo:income_list')

    def form_valid(self, form):
        self.object = income = form.save()
        messages.info(self.request,
                      f'収入を更新しました\n'
                      f'日付:{income.date}\n'
                      f'カテゴリ:{income.category}\n'
                      f'金額:{income.price}円')
        return redirect(self.get_success_url())


class PaymentDelete(generic.DeleteView):
    """支出削除"""
    template_name = 'kakeibo/delete.html'
    model = Payment

    def get_success_url(self):
        return reverse_lazy('kakeibo:payment_list')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['page_title'] = '支出削除確認'

        return context

    def delete(self, request, *args, **kwargs):
        self.object = payment = self.get_object()

        payment.delete()
        messages.info(self.request,
                      f'支出を削除しました\n'
                      f'日付:{payment.date}\n'
                      f'カテゴリ:{payment.category}\n'
                      f'金額:{payment.price}円')
        return redirect(self.get_success_url())


class IncomeDelete(generic.DeleteView):
    """収入削除"""
    template_name = 'kakeibo/delete.html'
    model = Income

    def get_success_url(self):
        return reverse_lazy('kakeibo:income_list')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['page_title'] = '収入削除確認'

        return context

    def delete(self, request, *args, **kwargs):
        self.object = income = self.get_object()
        income.delete()
        messages.info(self.request,
                      f'収入を削除しました\n'
                      f'日付:{income.date}\n'
                      f'カテゴリ:{income.category}\n'
                      f'金額:{income.price}円')
        return redirect(self.get_success_url())


更新のテンプレートとフォームは作成の時に使ったものを使いまわしています。

次に削除確認ページのdelete.htmlを作っていきます。
こちらも支出と収入で同じものを使います。

<!-- kakeibo/templates/kakeibo/delete.html -->
{% extends 'kakeibo/base.html' %}

{% block content %}
<h1>{{ page_title }}</h1>
<p class="mt-5"> 本当に削除してよろしいですか?</p>

<p class="mt-4">日付:{{ object.date }}</p>
<p class="mt-4">カテゴリ:{{ object.category }}</p>
<p class="mt-4">金額:{{ object.price }}</p>
<p class="mt-4">摘要: {% if object.description %}{{ object.description }}{% endif %}</p>

<form class="mt-4" method="post">
  {% csrf_token %}
  <!-- ページタイトルから戻ろ一覧を分岐 -->
  {% if '支出' in page_title %}
  <a class="btn btn-info" href="{% url 'kakeibo:payment_list'%}">
    いいえ、一覧に戻ります
  </a>
  {% else %}
  <a class="btn btn-info" href="{% url 'kakeibo:income_list'%}">
    いいえ、一覧に戻ります
  </a>
  {% endif %}
  <button type="submit" class="btn btn-danger">
    はい、削除します
  </button>
</form>

{% endblock %}


viewから渡されたpage_titleに支出の文字列が含まれているからどうかで、一覧ページの戻り先を指定しています。
支出という文字が含まれていたら、支出一覧へ、そうじゃなかったら収入一覧へ戻るという形です。

次に一覧ページを編集して、更新と削除のリンクボタンを表示しましょう。

<!-- kakeibo/templates/kakeibo/payment_list.html -->
...
<!-- 一覧表示 -->
<table class="table mt-3">
  <tr>
    <th>日付</th>
    <th>カテゴリ</th>
    <th>金額</th>
    <th>摘要</th>
    <!-- 追加 -->
    <th>編集</th> 
  </tr>
  {% for payment in payment_list %}
  <tr>
    <td>{{ payment.date }}</td>
    <td>{{ payment.category }}</td>
    <td>{{ payment.price|intcomma}}</td>
    <td>
      {% if payment.description %}
      {{ payment.description }}
      {% endif %}
    </td>
    <!-- 追加 -->
    <td>
      <div class="manage-btn-area">
        <div class="update-btn-area">
          <a class="btn btn-info" href="{% url 'kakeibo:payment_update' payment.pk %}">更新</a>
        </div>
        <div class="delete-btn-area">
          <a class="btn btn-danger" href="{% url 'kakeibo:payment_delete' payment.pk %}">削除</a>
        </div>
      </div>
    </td>
  </tr>
  {% endfor %}
</table>
...


収入一覧も同様です。

<!-- kakeibo/templates/kakeibo/income_list.html -->
...
<table class="table mt-3">
  <tr>
    <th>日付</th>
    <th>カテゴリ</th>
    <th>金額</th>
    <th>摘要</th>
    <th>編集</th>
  </tr>
  {% for income in income_list %}
  <tr>
    <td>{{ income.date }}</td>
    <td>{{ income.category }}</td>
    <td>{{ income.price|intcomma}}</td>
    <td>
      {% if income.description %}
      {{ income.description }}
      {% endif %}
    </td>
    <td>
      <div class="manage-btn-area">
        <div class="update-btn-area">
          <a class="btn btn-info" href="{% url 'kakeibo:income_update' income.pk %}">更新</a>
        </div>
        <div class="delete-btn-area">
          <a class="btn btn-danger" href="{% url 'kakeibo:income_delete' income.pk %}">削除</a>
        </div>
      </div>
    </td>
  </tr>
  {% endfor %}
</table>
...


新しいstyleの追記もしましょう。

/* kakeibo/static/kakeibo/css/style.css */

/* --------------------------------
 * ボタン
 * -------------------------------- */

...

/* 追加 */
.btn-danger, a.btn-danger {
  background-color: #f93154;
  color: #fff;
}

...

/* --------------------------------
 * 一覧ページのボタン部分
 * -------------------------------- */
/* 通常は横並び */
.manage-btn-area {
  display: flex;
  flex-direction: row;
  justify-content: center;
}

.delete-btn-area {
  margin-left: 5px;
}

/* 760px以下は縦並び */
@media (max-width: 760px) {
  .manage-btn-area {
    flex-direction: column;
  }

  .delete-btn-area {
    margin-left: 0px;
    margin-top: 5px;
  }
}





細かいところですが、window幅が狭まるとボタンが縦に並ぶようにしています。



今回はここまでにします。
次回は月間支出のダッシュボードを作成していきます。

TOPページ