naritoブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

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

プログラミング関連 Django Python 約151日前
2017年7月13日14:04
/adminの管理画面では、リレーション先のデータをポップアップで追加することができ、それがすぐに元ウィンドウに反映されます。
今回はこれをシンプルに実装していきます。

登録画面です。


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


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


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


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




Django1.11
Python3.6
です。

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)



これらは通常の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.pkはvalue="1"の1にあたります。
add_categoryは、JavaScriptの関数名になります。後で使います。
タグのform_validも同様です。
    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.htmlのadd_category関数に引数をつけて呼んでいるわけです。
その後、window.close();でこのウィンドウが閉じます。
このhtmlは表示されてすぐに閉じられるので、htmlの見た目等は気にしなくてよいでしょう。
<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>



まとめますと、
・ポップアップ用のビューを呼び出す
・データを追加し、データの表示名、データのpk、JavaScriptの関数名をclose.htmlに渡す
・close.htmlでは、post_form.htmlのadd_category等の関数にpkと表示名を渡し、閉じる
・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();