0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Django】 Formsetを使ってテーブルの中にフォームを入れる

Posted at

この記事で伝えたいこと

  • 画像のような画面の作り方
    Alt text

結論

Formsetとget_context_dataメソッドを使う

app1/views.py
from django.views.generic import TemplateView
from django.http import HttpResponse
from django import forms
from django.shortcuts import render, redirect

from .models import TestData


class IndexView(TemplateView):
    template_name: str = "app1/index.html"
    object_list = TestData.objects.all().order_by("number")
    max_num = object_list.count()

    # モデルフォームセット
    test_data_formset = forms.modelformset_factory(
        model=TestData,
        fields=["quantity"],
        extra=3,  # default-> 1
        max_num=max_num
    )

    def get_context_data(self, **kwargs):
        # get処理だけ書く
        ctx = super().get_context_data(**kwargs)
        # 新規作成formを作る場合は
        # formset = self.test_data_formset(queryset=TestData.objects.none())
        ctx["formset"] = self.test_data_formset()
        ctx["object_list"] = self.object_list
        return ctx

    def post(self, request, *args, **kwargs) -> HttpResponse:
        formset = self.test_data_formset(request.POST)
        if formset.is_valid():
            formset.save()
            return redirect("app1:index")
        context = {
            "object_list": self.object_list,
            "formset": formset,
        }
        return render(request, self.template_name, context)

ソースコード

ソースコードをGitHubに公開しています
https://github.com/ANKM0/django_sample_make_form_and_list_with_templateview.git

環境

  • django4.1.3
  • クラスベースビューを使います

フォルダ構成

フォルダ構成はこんな感じになっています
プロジェクト名はconfig,アプリ名はapp1です

DJANGO_SAMPLE(root)
│  db.sqlite3
│  gen_secrets.py
│  manage.py
│  secrets.json
│
├─app1
│  │  admin.py
│  │  apps.py
│  │  forms.py
│  │  models.py
│  │  tests.py
│  │  urls.py
│  │  views.py
│  │  __init__.py
│  │
│  ├─migrations
│  │  │  0001_initial.py
│  │  │  __init__.py
│  │  │
│  │  └─__pycache__
│  │          0001_initial.cpython-310.pyc
│  │          __init__.cpython-310.pyc
│  │
│  └─__pycache__
│          略
│
├─config
│  │  asgi.py
│  │  settings.py
│  │  urls.py
│  │  wsgi.py
│  │  __init__.py
│  │
│  └─__pycache__
│          略
│
└─templates
    └─app1
            base.html
            index.html

下準備

models,formsの作成

Formを作成する際に使うmodelsとFormsを作成します

app1/models.py
from django.db import models


class TestData(models.Model):
    number = models.PositiveIntegerField()
    name = models.CharField(max_length=200)
    price = models.PositiveIntegerField()
    quantity = models.PositiveIntegerField(blank=True, null=True)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = "テストデータ"
app1/forms.py
from django import forms
from .models import TestData


class QuantityForm(forms.Form):
    quantity = forms.IntegerField(
        label="price",
        min_value=0,
        required=False,
    )

ルーティング設定

ルーティングも設定しておきます

cofig/urls.py
from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('app1.urls')),
]
app1/urls.py
from django.urls import path
from . import views


app_name = "app1"
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
]

templateのbaseを作成

templateに使うbase.htmlを作成します

templates/app1/base.html
<!DOCTYPE html>
{% load static %}

<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    {% block meta %}{% endblock %}

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>

    <!-- FontAwesome-->
    <link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous" />

    <!-- https://github.com/yubinbango/yubinbango -->
    <script src="https://yubinbango.github.io/yubinbango/yubinbango.js" charset="UTF-8"></script>

    <link rel="stylesheet" href="{% static 'css/app1/main.css' %}">
    {% block csslink %}{% endblock %}

    <title>{% block title %}{% endblock %}</title>
</head>



<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <header class="mb-4">
        <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
            <div><p class="navbar-brand">ヘッダー</p></div>
        </nav>
    </header>

    <main class="mb-5">
        {% block content %}{% endblock %}
    </main>

    <footer class="fixed-bottom">
        <div class="container">
        </div>
    </footer>

</body>
</html>

本題 フォームinテーブルの作り方

下準備が終わったので
フォームをテーブルの中に入れる方法を説明していきます

方法その1 フォーム画面とロジックを自作する

Form画面とロジックのコードを書く方法です

templates/app1/index.html
{% extends "app1/base.html" %}
{% block title %}index{% endblock %}

{% block content %}
<div class="container">
    <h1>index page</h1>
    <br>
    <br>
    {{error_list.0}}

    <form class="form" action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}

        <div class="bg-light">
            <table class="table table-bordered ">
                <thead>
                    <tr class="table-light">
                        <th scope="col">商品番号</th>
                        <th scope="col">商品名</th>
                        <th scope="col">価格</th>
                        <th scope="col"></th>
                    </tr>
                </thead>
                <tbody>
                    {% for item in object_list %}
                        <tr class="bg-light">
                            <td>
                                {{ item.number }}
                            </td>
                            <td>
                                {{ item.name }}
                            </td>
                            <td>
                                {{ item.price }}
                            </td>
                            <td>
                                <div class="form-group">
                                    <input class="form-control" type="number" name="quantity_{{forloop.counter}}"
                                    {% if item.quantity == 0 %}
                                        value=""
                                    {% else %}
                                        value="{{item.quantity}}"
                                    {% endif %}
                                    min="0">
                                </div>
                            </td>
                        </tr>

                    {% endfor %}
                </tbody>
            </table>
        </div>
        <div class="text-right">
            <button type="submit" value="送信">送信</button>
        </div>

    </form>
</div>
{% endblock %}
app1/urls.py
from django.views.generic import TemplateView
from django.http import HttpResponse
from django.shortcuts import render, redirect


from .models import TestData
from .forms import QuantityForm


class IndexView(TemplateView):
    template_name: str = "app1/index.html"
    form_class = QuantityForm

    def get(self, request, *args, **kwargs) -> HttpResponse:
        object_list = TestData.objects.all().order_by("number")
        context = {
            "object_list": object_list,
        }
        return render(request, self.template_name, context)

    def post(self, request, *args, **kwargs) -> HttpResponse:
        quantity_list: list[str] = []
        error_list: int[str] = []

        object_list = TestData.objects.all().order_by("number")
        for quantity_id in range(1, object_list.count()+1):
            raw_quantity: str = request.POST.get(f"quantity_{quantity_id}")
            if raw_quantity == "":
                quantity_list += [-404]
            elif int(raw_quantity) < 0:
                quantity_list += [-403]
            else:
                quantity_list += [int(raw_quantity)]

        if len([i for i, x in enumerate(quantity_list) if x == -404]) == len(quantity_list):
            error_list += ["数を入力してください"]
        elif -403 in quantity_list:
            error_list += ["0以下の数が入力されています"]

        if len(error_list) > 0:
            object_list = TestData.objects.all().order_by("number")
            context = {
                "object_list": object_list,
                "error_list": error_list,
            }
            return render(request, self.template_name, context)

        for counter in range(0, len(quantity_list)):
            print("---------------")
            quantity: int = quantity_list[counter]
            print(f"quantity_list:{quantity_list}")
            print(f"quantity:{quantity}")

            if not quantity == -404 or quantity == 0:
                object = TestData.objects.get(number=counter+1)
                object.quantity = quantity
                object.save()
        return redirect("app1:index")

formで必要な処理
入力画面を表示->バリデーション->DBに登録
のうち、

  • 入力画面
  • DBに登録する処理

の2つを実装しています

この方法だと、処理を都度書く必要があるのでコードが汚く(冗長に)なります
そこで、Formsetを使います

方法その2 Formsetを使う

※Django-Boostを使っています

templates/app1/base.html
{% extends 'app1/base.html' %}
{% load boost %}
{% block title %}index{% endblock %}

{% block content %}
<div class="container">
    <h1>index page</h1>
    <br>
    <br>
    {{error_list.0}}

    <form class="form" action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        {{ formset.management_form }}

        <div class="bg-light">
            <table class="table table-bordered ">
                <thead>
                    <tr class="table-light">
                        <th scope="col">商品番号</th>
                        <th scope="col">商品名</th>
                        <th scope="col">価格</th>
                        <th scope="col"></th>
                    </tr>
                </thead>
                <tbody>
                    {% for item, form in object_list|zip:formset %}
                        <tr class="bg-light">
                            <td>
                                {{ item.number }}
                            </td>
                            <td>
                                {{ item.name }}
                            </td>
                            <td>
                                {{ item.price }}
                            </td>
                            <td>
                                {{ form.non_field_errors }}
                                <div class="form-group">
                                    <label for="{{ form.quantity.id_for_label }}">{{ form.quantity.label_tag }}</label>
                                {{ form.quantity }}
                                {{ form.quantity.errors }}

                                {% for field in form.hidden_fields %}
                                    {{ field }}
                                {% endfor %}
                                </div>
                            </td>
                        </tr>
                    {% endfor %}
                    {% for field in form.hidden_fields %}
                        {{ field }}
                    {% endfor %}
                </tbody>
            </table>
        </div>
        <div class="text-right">
            <button type="submit" class="btn btn-primary">送信</button>
        </div>

    </form>
</div>
{% endblock %}
app1/urls.py

from django.views.generic import TemplateView
from django.http import HttpResponse
from django import forms
from django.shortcuts import render, redirect

from .models import TestData


class IndexView(TemplateView):
    template_name: str = "app1/index.html"
    object_list = TestData.objects.all().order_by("number")
    max_num = object_list.count()

    # フォームセット(モデルと紐づけたいので、モデルフォームセットを使用)
    test_data_formset = forms.modelformset_factory(
        model=TestData,
        fields=["quantity"],
        extra=3,  # default-> 1
        max_num=max_num    # object_list.count()が3を返すとき、initial含めformは最大4となる
    )

    def get(self, request, *args, **kwargs) -> HttpResponse:
        # 新規作成formを作る場合は下のコメントアウトのように記述
        # formset = self.test_data_formset(queryset=TestData.objects.none())
        formset = self.test_data_formset()
        context = {
            "object_list": self.object_list,
            "formset": formset,
        }
        return render(request, self.template_name, context)

    def post(self, request, *args, **kwargs) -> HttpResponse:
        formset = self.test_data_formset(request.POST)
        if formset.is_valid():
            formset.save()
            return redirect("app1:index")
        context = {
            "object_list": self.object_list,
            "formset": formset,
        }
        return render(request, self.template_name, context)

Formsetを使用することで、必要なコードを少なくできました

しかし、getメソッドとpostメソッドで処理が重複しています
そこで、get_context_dataメソッドを使い、更にきれいなコードにしていきます

方法3 Formset+get_context_dataメソッド

app1/urls.py
from django.views.generic import TemplateView
from django.http import HttpResponse
from django import forms
from django.shortcuts import render, redirect

from .models import TestData


class IndexView(TemplateView):
    template_name: str = "app1/index.html"
    object_list = TestData.objects.all().order_by("number")
    max_num = object_list.count()

    test_data_formset = forms.modelformset_factory(
        model=TestData,
        fields=["quantity"],
        extra=3,
        max_num=max_num
    )

    def get_context_data(self, **kwargs):
        # get処理だけ書く
        ctx = super().get_context_data(**kwargs)
        # 新規作成formを作る場合は
        # formset = self.test_data_formset(queryset=TestData.objects.none())
        ctx["formset"] = self.test_data_formset()
        ctx["object_list"] = self.object_list
        return ctx

    def post(self, request, *args, **kwargs) -> HttpResponse:
        formset = self.test_data_formset(request.POST)
        if formset.is_valid():
            formset.save()
            return redirect("app1:index")
        context = {
            "object_list": self.object_list,
            "formset": formset,
        }
        return render(request, self.template_name, context)

ctx["キー"] = self.データと書くことでIndexViewクラスにデータを登録しています

get_context_dataはgetのときだけ呼び出されるので
formset = self.test_data_formset(request.POST)とすることでpostされたデータを渡しています

まとめ

フォームをテーブルの中に入れるには、Formsetとget_context_dataメソッドを使う

参考文献

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?