pyhaya’s diary

プログラミング、特にPythonについての記事を書きます。Djangoや機械学習などホットな話題をわかりやすく説明していきたいと思います。

Djangoで家計簿のWebアプリケーションを作る 7 ビューをクラスを使って整理する

Djangoで家計簿のWebアプリケーションを作っています。

ビューが汚い

ここまで様々な機能を実装してきました。その結果、views.pyの中身がだいぶ見づらくなってしまっています。

money/views.py

import calendar
import datetime
from django.shortcuts import render, redirect
from django.utils import timezone
import matplotlib.pyplot as plt
import pytz
from .models import Money
from .forms import SpendingForm
plt.rcParams['font.family'] = 'IPAPGothic'    #日本語の文字化け防止
# Create your views here.
TODAY = str(timezone.now()).split('-')

def index(request, year=TODAY[0], month=TODAY[1]):
    money = Money.objects.filter(use_date__year=year,                                                                                                                               use_date__month=month).order_by('use_date')

    total = index_utils.calc_month_pay(money)
    index_utils.format_date(money)
    form = SpendingForm()
    next_year, next_month = get_next(year, month)
    prev_year, prev_month = get_prev(year, month)

    context = {'year' : year,
            'month' : month,
            'prev_year' : prev_year,
            'prev_month' : prev_month,
            'next_year' : next_year,
            'next_month' : next_month,
            'money' : money,
            'total' : total,
            'form' : form
            }

    draw_graph(year, month)

    if request.method == 'POST':
        data = request.POST
        use_date = data['use_date']
        cost = data['cost']
        detail = data['detail']
        category = data['category']
        use_date = timezone.datetime.strptime(use_date, "%Y/%m/%d")
        tokyo_timezone = pytz.timezone('Asia/Tokyo')
        use_date = tokyo_timezone.localize(use_date)
        use_date += datetime.timedelta(hours=9)

        Money.objects.create(
                use_date = use_date,
                detail = detail,
                cost = int(cost),
                category = category,
                )
        return redirect(to='/money/{}/{}'.format(year, month))

    return render(request, 'money/index.html', context)

#...

どうにかしましょう。

リファクタリング

ビューの機能とは無関係の部分を抽出する

まずはビュー本来の機能である、コンテクストをHTMLに送るということ以外のことをしている部分を探し出して抽出していきましょう。まずはその月の支出合計を計算する部分は抽出できそうです。同様にして、データベースから日付をとってきて表示用に整形する部分も抽出できそうです。

money/utils/index_utils.py

def calc_month_pay(money):
    total = 0
    for m in money:
        total += m.cost

    return total

def format_date(money):
    for m in money:
        date = str(m.use_date).split(' ')[0]
        m.use_date = '/'.join(date.split('-')[1:3])

    return None

ついでに前月と次月を計算する関数もこちらに移してしまいましょう。

money/utils/index_utils.py

def calc_month_pay(money):
    total = 0
    for m in money:
        total += m.cost

    return total

def format_date(money):
    for m in money:
        date = str(m.use_date).split(' ')[0]
        m.use_date = '/'.join(date.split('-')[1:3])

    return None


def get_next(year, month):
    year = int(year)
    month = int(month)

    if month == 12:
        return str(year + 1), '1'
    else:
        return str(year), str(month + 1)

def get_prev(year, month):
    year = int(year)
    month = int(month)
    if month == 1:
        return str(year - 1), '12'
    else:
        return str(year), str(month - 1)

money/views.py

def index(request, year=TODAY[0], month=TODAY[1]):
    money = Money.objects.filter(use_date__year=year,                                                                                                                               use_date__month=month).order_by('use_date')

    total = index_utils.calc_month_pay(money)
    index_utils.format_date(money)
    form = SpendingForm()
    next_year, next_month = index_utils.get_next(year, month)
    prev_year, prev_month = index_utils.get_prev(year, month)

    context = {'year' : year,
            'month' : month,
            'prev_year' : prev_year,
            'prev_month' : prev_month,
            'next_year' : next_year,
            'next_month' : next_month,
            'money' : money,
            'total' : total,
            'form' : form
            }

    draw_graph(year, month)

    if request.method == 'POST':
        data = request.POST
        use_date = data['use_date']
        cost = data['cost']
        detail = data['detail']
        category = data['category']
        use_date = timezone.datetime.strptime(use_date, "%Y/%m/%d")
        tokyo_timezone = pytz.timezone('Asia/Tokyo')
        use_date = tokyo_timezone.localize(use_date)
        use_date += datetime.timedelta(hours=9)

        Money.objects.create(
                use_date = use_date,
                detail = detail,
                cost = int(cost),
                category = category,
                )
        return redirect(to='/money/{}/{}'.format(year, month))

    return render(request, 'money/index.html', context)

ビュークラスを使う

これで少しはすっきりしましたが、通常表示されるときに実行される部分と、postを受け取ったときに実行される部分が混ざってしまっています。これを解決するには、ビューをクラスとして書きます。

money/views.py

import calendar    
import datetime
from django.shortcuts import render, redirect
from django.utils import timezone
from django.views import View
import matplotlib.pyplot as plt
import pytz

from .models import Money
from .forms import SpendingForm
from .utils import index_utils

plt.rcParams['font.family'] = 'IPAPGothic'

TODAY = str(timezone.now()).split('-')

class MainView(View):
    def get(self, request, year=TODAY[0], month=TODAY[1]):
        money = Money.objects.filter(use_date__year=year,    
                            use_date__month=month).order_by('use_date')

        total = index_utils.calc_month_pay(money)
        index_utils.format_date(money)
        form = SpendingForm()
        next_year, next_month = index_utils.get_next(year, month)
        prev_year, prev_month = index_utils.get_prev(year, month)

        context = {'year' : year,
                'month' : month,
                'prev_year' : prev_year,
                'prev_month' : prev_month,
                'next_year' : next_year,
                'next_month' : next_month,
                'money' : money,
                'total' : total,
                'form' : form
                }

        draw_graph(year, month)

        return render(request, 'money/index.html', context)

    def post(self, request, year=TODAY[0], month=TODAY[1]):
        data = request.POST
        use_date = data['use_date']
        cost = data['cost']
        detail = data['detail']
        category = data['category']

        use_date = timezone.datetime.strptime(use_date, "%Y/%m/%d")
        tokyo_timezone = pytz.timezone('Asia/Tokyo')
        use_date = tokyo_timezone.localize(use_date)
        use_date += datetime.timedelta(hours=9)

        Money.objects.create(
                use_date = use_date,
                detail = detail,
                cost = int(cost),
                category = category,
                )
        return redirect(to='/money/{}/{}'.format(year, month)) 

#...

ここでは最も一般的なdjango.views.Viewをテンプレートビューとして使っています。この変更に伴って、urls.pyも少し変更する必要があります。

money/urls.py

from django.urls import path
from . import views

app_name = 'money'
urlpatterns = [
        path('', views.MainView.as_view(), name='index'),
        path('<int:year>/<int:month>', views.MainView.as_view(), name='index'),
        ]

このようにas_viewを付けることによってクラスをビューとして呼び出すことができます。