Django、ポップアップでデータを追加

Python - Django
2018年11月19日2:44に更新(約9時間前)
2018年11月7日14:36に作成(約11日前)

旧ブログ移行記事です。

/adminの管理画面では、リレーション先のデータをポップアップで追加することができ、それがすぐに元ウィンドウに反映されます。 今回はこれをシンプルに実装していきます。

以下のような登録画面があり...

+のリンクを押すと新しいウィンドウが開き、リレーション先のモデルの追加画面になります。 これはForeignKeyで紐付いたモデルです。

適当に名前を入れて追加を押すと...

新しいウィンドウが自動で閉じ、元のウィンドウに追加したデータが反映されます。

ManyToManyでも、同様の動作になります。

models.py

Postから、ForeignKeyでCategory、ManyToManyでTagが紐付いています。

from django.db import models


class Category(models.Model):
    """カテゴリ"""
    name = models.CharField('カテゴリ名', max_length=255)

    def __str__(self):
        return self.name


class Tag(models.Model):
    """タグ"""
    name = models.CharField('タグ名', max_length=255)

    def __str__(self):
        return self.name


class Post(models.Model):
    """記事"""
    title = models.CharField('タイトル', max_length=255)
    text = models.TextField('本文')
    category = models.ForeignKey(
        Category, verbose_name='カテゴリ', on_delete=models.PROTECT,
    )
    tag = models.ManyToManyField(Tag, verbose_name='タグ')

    def __str__(self):
        return self.title

urls.py

記事一覧、記事作成、ポップアップでのカテゴリ作成、ポップアップでのタグ作成の4つです。

from django.conf.urls import url
from app import views

app_name = 'app'

urlpatterns = [
    url(r'^$', views.PostList.as_view(), name='post_list'),
    url(r'^post_create/$', views.PostCreate.as_view(), name='post_create'),
    url(r'^popup/category_create/$',
        views.PopupCategoryCreate.as_view(),
        name='popup_category_create'
    ),
    url(r'^popup/tag_create/$',
        views.PopupTagCreate.as_view(),
        name='popup_tag_create'
    ),
]

views.py

from django.views import generic
from django.shortcuts import render
from django.urls import reverse_lazy
from .models import Post, Category, Tag


class PostList(generic.ListView):
    """記事一覧"""
    model = Post


class PostCreate(generic.CreateView):
    """記事の作成"""
    model = Post
    fields = '__all__'
    success_url = reverse_lazy('app:post_list')


class CategoryCreate(generic.CreateView):
    """カテゴリの作成"""
    model = Category
    fields = '__all__'
    success_url = reverse_lazy('app:post_list')


class TagCreate(generic.CreateView):
    """タグの作成"""
    model = Tag
    fields = '__all__'
    success_url = reverse_lazy('app:post_list')


class PopupCategoryCreate(CategoryCreate):
    """ポップアップでのカテゴリ作成"""

    def form_valid(self, form):
        category = form.save()
        context = {
            'object_name': str(category),
            'object_pk': category.pk,
            'function_name': 'add_category'
        }
        return render(self.request, 'app/close.html', context)


class PopupTagCreate(TagCreate):
    """ポップアップでのタグ作成"""

    def form_valid(self, form):
        tag = form.save()
        context = {
            'object_name': str(tag),
            'object_pk': tag.pk,
            'function_name': 'add_tag'
        }
        return render(self.request, 'app/close.html', context)

以下の2つは通常のCreateViewです。ポップアップではない通常の作成ページが欲しい場合のために定義しておきます。

class CategoryCreate(generic.CreateView):
...
...
class TagCreate(generic.CreateView):

そして、ポップアップ用の作成ページも大体の処理は同じなので継承します。

class PopupCategoryCreate(CategoryCreate):
...
...
class PopupTagCreate(TagCreate):

close.htmlに、str(category)category.pk'add_category'という文字列を渡します。 <option value="1">カテゴリA</option>のようなoption要素を後で作成する必要があるのですが、str(category)カテゴリAの部分にあたり、category.pkvalue="1"の1にあたります。 add_categoryは、JavaScriptの関数名になります。後で使います。

    def form_valid(self, form):
        category = form.save()
        context = {
            'object_name': str(category),
            'object_pk': category.pk,
            'function_name': 'add_category'
        }
        return render(self.request, 'app/close.html', context)

base.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>

category_form.html

カテゴリの作成ページ

{% extends 'app/base.html' %}
{% block content %}
    <form action="" method="POST">
        {{ form.as_p }}
        {%  csrf_token %}
        <button type="submit">送信</button>
    </form>
{% endblock %}

tag_form.html

タグの作成ページ

{% extends 'app/base.html' %}
{% block content %}
    <form action="" method="POST">
        {{ form.as_p }}
        {%  csrf_token %}
        <button type="submit">送信</button>
    </form>
{% endblock %}

post_list.html

記事の一覧

{% extends 'app/base.html' %}
{% block content %}
<a href="{% url 'app:post_create' %}">作成</a>
<hr>
    {% for post in post_list %}
        {{ post.title }}
        <hr>
    {% endfor %}
{% endblock %}

post_form.html

記事の作成ページです。重要な部分が2つあります。

{% extends 'app/base.html' %}
{% block content %}
    <form action="" method="POST">
        {{ form.title }}<br>
        {{ form.text }}<br>
        {{ form.category }}
        <a href="javascript:void(0);" onclick="window.open('{% url 'app:popup_category_create' %}','subwin','width=500,height=500');">+</a>
        <br>
        {{ form.tag }}
        <a href="javascript:void(0);" onclick="window.open('{% url 'app:popup_tag_create' %}','subwin','width=500,height=500');">+</a>
        <br>
        {%  csrf_token %}
        <button type="submit">送信</button>
    </form>
    <script>
        function add_category(name, pk){
            var select = document.getElementById('id_category');
            // <option value="pk">選択肢名</option> をつくる
            var option = document.createElement('option');
            option.setAttribute('value', pk);
            option.innerHTML = name;

            // カテゴリの先頭に追加し、選択済みにする
            select.add(option,0);
            select.options[0].selected= true;
        }

        function add_tag(name, pk){
            var select = document.getElementById('id_tag');
            // <option value="pk">選択肢名</option> をつくる
            var option = document.createElement('option');
            option.setAttribute('value', pk);
            option.innerHTML = name;

            // カテゴリの先頭に追加し、選択済みにする
            select.add(option,0);
            select.options[0].selected= true;
        }
    </script>
{% endblock %}

まず、このa要素で新規ウィンドウを開いています。上がカテゴリ用、下がタグ用のリンクです。

        <a href="javascript:void(0);" onclick="window.open('{% url 'app:popup_category_create' %}','subwin','width=500,height=500');">+</a>
        <br>
        {{ form.tag }}
        <a href="javascript:void(0);" onclick="window.open('{% url 'app:popup_tag_create' %}','subwin','width=500,height=500');">+</a>

以下のJavaScriptは、表示名とpkを受取り、カテゴリ・タグのselectタグに追加する関数です。 つまり<select>に、<option value="pk">選択肢名</option> を追加するための関数です。 この関数はclose.htmlから呼ばれます。

    <script>
        function add_category(name, pk){
            var select = document.getElementById('id_category');
            // <option value="pk">選択肢名</option> をつくる
            var option = document.createElement('option');
            option.setAttribute('value', pk);
            option.innerHTML = name;

            // カテゴリの先頭に追加し、選択済みにする
            select.add(option,0);
            select.options[0].selected= true;
        }

        function add_tag(name, pk){
            var select = document.getElementById('id_tag');
            // <option value="pk">選択肢名</option> をつくる
            var option = document.createElement('option');
            option.setAttribute('value', pk);
            option.innerHTML = name;

            // タグの先頭に追加し、選択済みにする
            select.add(option,0);
            select.options[0].selected= true;
        }
    </script>

close.html

新規ウィンドウでカテゴリ・タグを追加するとこのhtmlが返されます。

<html>
<head>
</head>
<body>
<script type="text/javascript">
var object_name = '{{ object_name }}';
var object_pk = '{{ object_pk }}';
window.opener.{{ function_name }}(object_name, object_pk);
window.close();
</script>
</body>
</html>

window.opener.{{ function_name }}(object_name, object_pk)は、カテゴリならば window.opener.add_category('カテゴリA', 1);のような呼び出しになります。post_form.htmladd_category関数に引数をつけて呼んでいるわけです。

その後、window.close();でこのウィンドウが閉じます。

このhtmlは表示されてすぐに閉じられるので、htmlの見た目等は気にしなくてよいでしょう。

まとめますと、

  1. ポップアップ用のビューを呼び出す
  2. データを追加し、データの表示名、データのpk、JavaScriptの関数名をclose.htmlに渡す
  3. close.htmlでは、post_form.htmladd_category等の関数にpkと表示名を渡し、閉じる
  4. post_form.htmlにて、selectタグに表示名:pk の要素を追加し、選択済みにする

という流れになります。

もっとシンプルな方法としては、close.htmlを以下のようにしておく方法があります。 post_form.htmlのJavaScript関数と、表示名、pk、関数名の受け渡しも不要です。

<html>
<head>
</head>
<body>
<script type="text/javascript">
window.opener.location.reload();
window.close();
</script>
</body>
</html>

window.opener.location.reload();で元のウィンドウを更新しているだけです。(F5を押して更新するのと同じ) この方法だと書きかけの内容が消えたり追加カテゴリ・タグを選択済みにできないのですが、それらが気にならない場合ではお勧めです。

window.opener.location.reload();
window.close();

記事にコメントする