今回はDjangoアプリケーションのモデルをエクスポートする方法についての備忘記事です。
ブログアプリを想定して、カテゴリとタイトルだけの簡単なモデルを用意しました。
アプリケーション名はapp
としています。
# app/models.py
from django.db import models
class Category(models.Model):
name = models.CharField('カテゴリ名', max_length=12)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField('タイトル', max_length=32)
category = models.ForeignKey(Category, on_delete=models.PROTECT)
def __str__(self):
return self.title
まずは通常のcsvエクスポートをしてみましょう。
アプリ内のurls.pyを以下のようにします。
# app/urls.py
from django.urls import path
from . import views
app_name = 'app'
urlpatterns = [
path('', views.Top.as_view(), name='top'),
path('export/', views.export, name='export'),
]
トップページとエクスポート用のパターンを作成しました。
次にviews.pyを編集します。
# app/views.py
from django.views import generic
from .models import Post
from django.http import HttpResponse
import csv
class Top(generic.ListView):
model = Post
def export(request):
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="posts.csv"'
writer = csv.writer(response)
for post in Post.objects.all():
writer.writerow([post.pk, post.title, post.category])
return response
次に表示させるテンプレートを作っていきます。
まずは、base.htmlです。
今回はbootstrapを使っています。
<!-- app/templates/app/base.html -->
<!doctype html>
<html lang="ja">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<title>Export Test</title>
</head>
<body>
<!-- header -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="{% url 'app:top' %}">Export Test</a>
<div class="navbar-nav">
<a class="nav-item nav-link active" href="{% url 'app:top' %}">Home <span class="sr-only">(current)</span></a>
<a class="nav-item nav-link active" href="/admin">Admin <span class="sr-only">(current)</span></a>
</div>
</nav>
<!-- content -->
<div class="container">
{% block content %}{% endblock %}
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</body>
</html>
次にpost_list.htmlを作成し、以下のようにします。
<!-- app/templates/app/post_list.html -->
{% extends "app/base.html" %}
{% block content %}
<div class="mt-4">
<a class="btn btn-primary" href="{% url 'app:export' %}">export</a>
{% for post in post_list %}
<article class="mt-4">
<span class>{{ post.category }}</span>
<h1>{{ post.title }}</h1>
</article>
{% endfor %}
</div>
{% endblock %}
とりあえず記事の一覧とexport用のボタンを用意しています。
exportボタンを押すとviews.pyの関数が呼ばれ、csvファイルがダウンロードされます。
csvのヘッダーを付けるには以下のようにします。
# app/views.py
def export(request):
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="posts.csv"'
writer = csv.writer(response)
# 追加
header = ['pk', 'title', 'category']
writer.writerow(header)
for post in Post.objects.all():
writer.writerow([post.pk, post.title, post.category])
return response
この方法ですと、Postモデルの全件がダウンロードされます。
しかし何らかの検索を一覧画面で実行して、結果だけを出力したい、というケースがあると思います。
実装をする前に、簡単な検索フォームを作ります。
アプリ内にforms.pyを作成して以下のようにします。
# app/forms.py
from django import forms
from .models import Category, Post
class PostSearchForm(forms.Form):
key_word = forms.CharField(
label='検索キーワード',
required=False,
widget=forms.TextInput(attrs={'class': 'form-control',
'autocomplete': 'off',
'placeholder': 'Keyword',
})
)
category = forms.ModelChoiceField(
label='カテゴリ',
required=False,
queryset=Category.objects.order_by('name'),
widget=forms.Select(attrs={'class': 'form-control'})
)
キーワードとカテゴリだけの簡単な検索です。
そして、views.pyで読み込みます。
# app/views.py
from .forms import PostSearchForm # 追加
class Top(generic.ListView):
model = Post
def get_queryset(self):
queryset = super().get_queryset()
self.form = form = PostSearchForm(self.request.GET or None)
if form.is_valid():
key_word = form.cleaned_data.get('key_word')
if key_word:
for word in key_word.split():
queryset = queryset.filter(title__icontains=word)
category = form.cleaned_data.get('category')
if category:
queryset = queryset.filter(category=category)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_form'] = self.form
return context
次にpost_list.htmlでこのsearch_formを表示します。
<!-- app/templates/app/post_list.html -->
{% extends "app/base.html" %}
{% block content %}
<div class="mt-4">
<a class="btn btn-primary" href="{% url 'app:export' %}">export</a>
<!-- 追加 -->
<form class="mt-2" id="search-form" name="search-form" method="GET">
<div class="row">
<div class="col-md-2">
{{ search_form.key_word }}
</div>
<div class="col-md-2">
{{ search_form.category }}
</div>
<div class="col-md-2">
<button class="btn btn-sm btn-info ml-4" type="submit">Search</button>
</div>
</div>
</form>
{% for post in post_list %}
<article class="mt-4">
<span class>{{ post.category }}</span>
<h1>{{ post.title }}</h1>
</article>
{% endfor %}
</div>
{% endblock %}
このように検索フォームが表示されます。
カテゴリにPythonを指定して検索を実行してみます。
こうなりますね。
前置きが長くなりましたが、この状態でexportボタンを押した際に、Pythonを始める方法
、というエントリだけダウンロードされてほしいわけです。
まず、post_list.html内のexportボタンを以下のようにします。
<!-- 変更 -->
<a class="btn btn-primary" href="{% url 'app:export' %}?{{ request.GET.urlencode }}">export</a>
こうすることで、key_word=&category=1
というようなパラメータをエクスポート関数に渡せます。
渡されたパラメータを確認してみましょう。
# app/views.py
def export(request):
print(request.GET)
>>>
<QueryDict: {'key_word': [''], 'category': ['1']}>
あとはこのQueryDictを元に絞り込みしたうえで出力をすればいいわけです。
# app/views.py
def export(request):
posts = Post.objects.all()
# リクエストに応じて絞り込み
key_word = request.GET.get('key_word')
if key_word:
for word in key_word.split():
posts = posts.filter(title__icontains=word)
category = request.GET.get('category')
if category:
posts = posts.filter(category=category)
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="posts.csv"'
writer = csv.writer(response)
header = ['pk', 'title', 'category']
writer.writerow(header)
for post in posts:
writer.writerow([post.pk, post.title, post.category])
return response
これで一覧画面の検索に応じて出力されるようになります。
次にExcel形式での出力を実装していきます。
実務の現場ではExcelは良く使われるので、覚えておいて損はありません。
openpyxlを使います。
pip install openpyxl
excel出力用のパターンを追加します。
# app/urls.py
urlpatterns = [
path('', views.Top.as_view(), name='top'),
path('export/', views.export, name='export'),
path('export_excel', views.export_excel, name='export_excel'), # 追加
]
次にviewです。
# app/views.py
import openpyxl # 追加
def export_excel(request):
# 新規ブックを作成
wb = openpyxl.Workbook()
ws = wb.active
response = HttpResponse(content_type='application/vnd.ms-excel')
response['Content-Disposition'] = 'attachment; filename=posts.xlsx'
ws.cell(1, 1).value = 'pk'
ws.cell(1, 2).value = 'title'
ws.cell(1, 3).value = 'category'
k = 2
for post in Post.objects.all():
ws.cell(k, 1).value = post.pk
ws.cell(k, 2).value = post.title
# .nameをつけないとバグ
ws.cell(k, 3).value = post.category.name
k += 1
wb.save(response)
return response
テンプレートにリンクボタンを設置します。
<!-- app/templates/app/post_list.html -->
...
<a class="btn btn-primary" href="{% url 'app:export' %}?{{ request.GET.urlencode }}">export</a>
<!-- 追加 -->
<a class="btn btn-success" href="{% url 'app:export_excel' %}">export excel</a>
...
ボタンを押すとExcelでダウンロードされます。
次にpandas DataFrameの出力です。
例えば何らかの複雑な処理をした結果を出力したい…というケースがあるかもしれません。
django-pandasを使うと、クエリセットを簡単にpandas DataFrame化できるので便利です。
pip install django-pandas
後はおなじみで以下のように編集していきます。
# app/urls.py
urlpatterns = [
path('', views.Top.as_view(), name='top'),
path('export/', views.export, name='export'),
path('export_excel', views.export_excel, name='export_excel'),
# 追加
path('export_pandas_df', views.export_pandas_dataframe, name='export_pandas_df')
]
# app/views.py
from django_pandas.io import read_frame # 追加
def export_pandas_dataframe(request):
df = read_frame(qs=Post.objects.all(), fieldnames=['pk', 'title', 'category'])
response = HttpResponse(content_type='text/csv; charset=utf8')
response['Content-Disposition'] = 'attachment; filename=posts.csv'
# 実際に使う場合はここで何らかの処理が入る
# とりあえず列名だけ変更
df = df.rename(columns={'pk': '番号', 'title': 'タイトル', 'category': 'カテゴリ'})
df.to_csv(path_or_buf=response, encoding='utf-8', index=None)
return response
<!-- app/templates/app/post_list.html -->
...
<a class="btn btn-primary" href="{% url 'app:export' %}?{{ request.GET.urlencode }}">export</a>
<a class="btn btn-success" href="{% url 'app:export_excel' %}">export excel</a>
<!-- 追加 -->
<a class="btn btn-warning" href="{% url 'app:export_pandas_df' %}">export pandas df</a>
...
今回は列名だけ変更してみました。
以下のように出力されるでしょう。
https://github.com/qlitre/django-export-test