今回はデータベースに保存されているデータをCSVファイルにエクスポートし、DjangoのモデルフィールドにあるFileFieldにファイルの保管先(ディレクトリ)を保存したのち、そのままCSVファイルをダウンロードできるという機能を構築したいと思います。
CSVファイルに保管する際は、Python標準ライブラリであるCSVモジュールか外部ライブラリのdjango-pandasを使用していきます。
ファイルの保管内容は、自動保管と手動保管の2つの実装をそれぞれ行っていきます。
自動保管では、データが作成、もしくわ更新される度に最新の状態でデータがCSVファイルに書き込まれます。
手動保管では、「ファイルの作成」というボタンを押すと最新の状態でデータがCSVファイルに書き込まれます。
これらの自動、もしくわ手動で作成されたCSVファイルは他のモデルで定義されているFileFieldへアップロードされ、そのままダウンロード可能な状態へと整います。
あとは煮るなり焼くなりしてもらえたら幸いです。
実行環境 |
---|
Windows Subsystem for Linux |
Python 3.6.9 |
pip 9.0.1 |
使用ライブラリ | ライセンス |
---|---|
Django==3.1 | BSD |
django-cleanup==5.0.0 | MIT |
django-pandas==0.6.2 | BSD |
使用ライブラリ中にある「django-cleanup」と「django-pandas」ですが、前者はアップロードされているファイルが削除、もしくわ更新された際に実際のファイルも削除してくれるモジュールです。
後者の「django-pandas」は、モデルインスタンスをそのまま渡すだけでテーブル形式の構造に変換をし、データの分析やCSVファイルの保管を容易に実装することができます。
それぞれインストールを終えたら、設定ファイルに追記します。
$ pip install django-cleanup django-pandas
各モジュールとアプリを設定し、アップロードされるファイルのディレクトリを設定します。
# config/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_cleanup.apps.CleanupConfig', # 設定
'django_pandas', # 設定
'app.apps.AppConfig', # アプリ名
]
....
STATIC_URL = '/static/'
import os
# ファイルの保管先ディレクトリ
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
ではsettings.pyの階層にある「urls.py」も編集します。
# config/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('app.urls')),
]
# 開発環境下でファイルを扱う際の設定
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
次にモデルの構築です。
イメージでは、TODOリストにカテゴリーが紐づいており、別途アップロード用のモデルがあります。
# app/models.py
from django.db import models
class Category(models.Model):
"""
カテゴリー
"""
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class Todo(models.Model):
"""
TODOリスト
"""
title = models.CharField(max_length=255)
category = models.ForeignKey(Category, on_delete=models.PROTECT)
created_at = models.DateField(auto_now_add=True)
def __str__(self):
return self.title
class FileUpload(models.Model):
"""
アップロード
"""
upload_dir = models.FileField(upload_to='%Y/%m/%d')
created_at = models.DateField(auto_now_add=True)
def __str__(self):
return str(self.upload_dirname)
アップロード用に定義したFileUploadモデルの「models.FileFiled」に関してはこちらの記事で詳しく触れています。
「admin.py」も編集しておきましょう。
# app/admin.py
from django.contrib import admin
from .models import Category, Todo, FileUpload
admin.site.register(Category)
admin.site.register(Todo)
admin.site.register(FileUpload)
次にアプリディレクトリ内の「urls.py」と「views.py」を編集します。
# app/urls.py
from django.urls import path
from . import views
app_name = 'app'
urlpatterns = [
path('', views.index, name='index'),
]
# app/views.py
from django.shortcuts import render
from .models import Todo, FileUpload
def index(request):
"""
トップページ
"""
todo_obj = Todo.objects.all()
# 保存データからのアップロード用としてフィルターをかけて取得
file_obj = FileUpload.objects.filter(upload_dir__icontains='auto_upload_file')
context = {
'todo_obj': todo_obj,
'file_obj': file_obj,
}
return render(request, 'app/index.html', context)
テンプレートファイルの「index.html」は以下のようになります。
<!-- app/templates/app/index.html -->
<!doctype html>
<html>
<head>
<title>テストアプリ</title>
</head>
<body>
<h2>Todoリスト</h2>
<br>
<!-- file_objにデータがあれば表示、無ければ「ありません」 -->
{% if file_obj %}
<h2>NEWファイル</h2>
{% for file in file_obj %}
<p>
{{ file.created_at }}
{{ file.upload_dir.name }}
<a href="{{ file.upload_dir.url }}">ダウンロード</a>
</p>
{% endfor %}
{% else %}
<h2>現在ファイルはありません</h2>
{% endif %}
<br>
<!-- TODOのリストを表示 -->
{% for todo in todo_obj %}
<p>
{{ todo.created_at }}
{{ todo.title }}
</p>
{% endfor %}
</body>
</html>
ではモデルをデータベースに反映させ、管理者として登録します。
$ python3 manage.py makemigrations app
$ python3 manage.py migrate app
$ python3 manage.py createsuperuser
幾つかデータを保存したら準備完了です。
アプリディレクトリ内に、保存データをCSVファイルへと処理するPythonファイル(「model_create_file.py」)を新規作成します。
app
|--__pycache__
|--migrations
|--templates
|--__init__.py
|--...
|--model_create_file.py # 新規作成
|--...
# app/model_create_file.py
from django.conf import settings
from .models import Todo, FileUpload
import os
# mediaルートとFileUploadモデルに渡す相対パス
path = os.path.join('auto_upload_file', 'todo.csv')
# ファイルを書き込む際に渡す絶対パス
media_path = os.path.join(settings.MEDIA_ROOT, path)
# 一時的にFileUploadモデル内に保存されている相対パスを取得
file_dir = FileUpload.objects.filter(upload_dir=path)
# Todoモデルのオブジェクトを取得
todo = Todo
def create_csv():
"""
CSVモジュールを使用してファイルを作成
"""
import csv
todo_head = todo._meta.get_fields() # フィールドのインスタンスを取得
todo_head_value = [row.name for row in todo_head] # フィールド名を取得
todo_obj = todo.objects.all()
# 各フィールドの要素をリスト内リストとして取得
todo_values = [[row.id, row.title, row.category.name, row.created_at] for row in todo_obj]
todo_values.insert(0, todo_head_value) # ヘッダー(列名)用にフィールド名を挿入
# 絶対パスに指定されたファイルへ要素を書き込む
with open(media_path, 'wt') as f:
csvout = csv.writer(f)
csvout.writerows(todo_values)
# 検索した相対パスが無ければ新規作成
if not file_dir:
FileUpload.objects.create(upload_dir=path)
「create_csv()」関数が呼ばれると、上から順番に処理が始まります。
関数内で定義されている処理は、CSVモジュールで処理できるように整えているだけです。
詳しくこちらの記事でも説明しています。
関数外(上部)では関数内で必要とされる要素を取得していますが、このあと「django-pandas」を使用したCSVファイルへ書き込む関数も定義するので、同じ要素を利用するために分けました。
ではdjango-pandasを使用した関数も定義しておきます。
settings.pyに「django_pandas」として明記されていれば呼び出すことができます。
# config/settings.py
INSTALLED_APPS = [
...
'django_pandas',
...
]
では「model_create_file.py」の下部に「django_pandas」を使用した関数を定義します。
# app/model_create_file.py
from django.conf import settings
from .models import Todo, FileUpload
import os
# mediaルートとFileUploadモデルに渡す相対パス
path = os.path.join('auto_upload_file', 'todo.csv')
# ファイルを書き込む際に渡す絶対パス
media_path = os.path.join(settings.MEDIA_ROOT, path)
# 一時的にFileUploadモデル内に保存されている相対パスを取得
file_dir = FileUpload.objects.filter(upload_dir=path)
# Todoモデルのオブジェクトを取得
todo = Todo
...
def pandas_csv():
"""
django-pandasを使用したCSVファイルの作成
"""
from django_pandas.io import read_frame
query = todo.objects.all()
df = read_frame(query) # インスタンスを渡す
df.to_csv(media_path) # CSVファイルの作成
# 検索した相対パスが無ければ新規作成
if not file_dir:
FileUpload.objects.create(upload_dir=path)
「pandas_csv()」関数内「df」変数名はpandasで使われているDataFrameの略で、出力すると以下のようなテーブル形式へと変換されます。
id title category created_at
0 18 test1 A 2020-08-26
1 19 test2 B 2020-08-26
2 20 test3 C 2020-08-26
3 21 test4 A 2020-08-26
4 22 test5 C 2020-08-26
DataFrame化された変数のメソッド「to_csv()」引数にファイル名までのルートパスを渡すだけでCSVファイルを作成することができます。
では、「model_create_file.py」には以下の2つの関数ができあがりました。
# app/model_create_file.py
....
....
def create_csv():
"""
CSVモジュールを使用したCSVファイル作成
"""
import csv
todo_head = todo._meta.get_fields()
todo_head_value = [row.name for row in todo_head]
todo_obj = todo.objects.all()
todo_values = [[row.id, row.title, row.category.name, row.created_at] for row in todo_obj]
todo_values.insert(0, todo_head_value)
with open(media_path, 'wt') as f:
csvout = csv.writer(f)
csvout.writerows(todo_values)
def pandas_csv():
"""
django-pandasを使用したCSVファイル作成
"""
from django_pandas.io import read_frame
query = todo.objects.all()
df = read_frame(query)
df.to_csv(media_path)
この双方どちらかの関数を呼び出すだけ、ファイルまでの相対パスをFileUploadモデルに保存し、そのルート上へファイルを書き込むようになります。
※今回削除といった機能は加えないため、管理画面等でFileUploadモデルの要素を削除します。
ではDjangoの「shell」コマンドを開いて実際に機能するか試してみます。
$ python3 manage.py shell
>>>
>>> from app.models import FileUpload
>>>
>>> # モデル内の要素が空であることを確認
>>> FileUpload.objects.all()
<QuerySet []>
>>>
>>> # CSVファイル作成用のファイルをインポート
>>> from app.model_create_file import create_csv, pandas_csv
>>>
>>> # CSVモジュールの関数を実行
>>> create_csv()
>>>
>>> # モデル内の要素を確認
>>> FileUpload.objects.all()
<QuerySet [<FileUpload: auto_upload_file/todo.csv>]>
>>>
>>> #モデル内の要素を一旦削除
>>> FileUpload.objects.all().delete()
(1, {'app.FileUpload': 1})
>>>
>>> # モデル内の要素が空であることを確認
>>> FileUpload.objects.all()
<QuerySet []>
>>>
>>> # django-pandasの関数を実行
>>> pandas_csv()
>>>
>>> # モデル内の要素を確認
>>> FileUpload.objects.all()
<QuerySet [<FileUpload: auto_upload_file/todo.csv>]>
>>>
>>> # shellを起動したままCSVファイルの中身を確認する
>>> import subprocess
>>>
>>> # Unixコマンドを指定し、ファイルの中身を取得
>>> subprocess.call(['cat', 'media/auto_upload_file/todo.csv'])
,id,title,category,created_at
0,18,test1,A,2020-08-26
1,19,test2,B,2020-08-26
2,20,test3,C,2020-08-26
3,21,test4,A,2020-08-26
4,22,test5,C,2020-08-26
5,23,test6,C,2020-08-26
6,24,test7,A,2020-08-26
7,25,test8,B,2020-08-26
8,26,test9,A,2020-08-26
9,27,test10,B,2020-08-26
10,28,test11,A,2020-08-26
11,29,test12,B,2020-08-27
0
>>>
>>> # モデル内の要素を一旦削除
>>> FileUpload.objects.all().delete()
(1, {'app.FileUpload': 1})
>>>
>>> quit()
しっかり機能していることが分かりました。
冒頭でも言いましたが、「django-cleanup」のおかげでアップロードされているファイル(相対パス)を削除するだけで、実際のファイルも削除されています。
この作成した関数をDjangoプロジェクトの任意の場所に当てはめることによってファイルが自動で作成されるのか、もしくわ手動で作成されるのかを実行できるようになります。
自動作成の仕組みは、管理画面、もしくわフォーム画面においてデータが保存された際に保存されているデータをCSVファイルに移してアップロードされるという流れです。
そのために、ModelFormを継承し、saveメソッドをオーバーライドして独自の処理を実行させる必要があります。
なので、アプリディレクトリ内に「forms.py」を新規作成します。
# app/forms.py
from django import forms
from .models import Todo
from .model_create_file import create_csv, pandas_csv
class TodoForm(forms.ModelForm):
"""
Todoモデルフォーム
"""
model = Todo
fields = '__all__'
def save(self, commit=True):
"""
カスタムメソッド
"""
todo_create = super(TodoForm, self).save(commit=False)
todo_create.save()
# データの保存が終わるとCSVファイル作成用の関数が呼ばれる
create_csv()
# pandas_csv()
return todo_create
ModelFormのsaveメソッドをカスタムする場合は、引数の「commi=False」を指定し、フィールドに渡された要素を取得してからデータを保存する必要があります。
そして戻り値として保存されたモデルインスタンスを渡します。
詳しくは公式ドキュメントにて
今回自作のフォーム画面は作成しませんが、このモデルフォールを管理画面で使えるように、adminのform属性に渡します。
それでは「admin.py」を編集します。
# app/admin.py
from django.contrib import admin
from .models import Category, Todo, FileUpload
from .forms import TodoForm
class TodoAdmin(admin.ModelAdmin):
form = TodoForm # form属性にオーバーライドされたフォームを代入
admin.site.register(Category)
admin.site.register(Todo, TodoAdmin) # 追記
admin.site.register(FileUpload)
これにて、Todoモデルに要素が保存されるとCSVファイルも一緒に作成されることになります。
適当な要素をTodoモデルに保存すると
FileUploadモデルにてファイルがアップロードされています。
もちろん外部のファイル(PCなど)を直接アップロードすることもできます。
トップページへ移動すると、以下のように表示されていることが分かります。
手動作成の仕組みは、テンプレートファイルにformタグを設置し、「request.method」が「POST」として呼ばれた際にCSVファイルが作成されます。
では「views.py」を編集します。
# app/views.py
from django.shortcuts import render, redirect
from .models import Todo, FileUpload
from .forms import FileUploadForm
from .model_create_file import create_csv, pandas_csv
def index(request):
"""
トップページ
"""
todo_obj = Todo.objects.all()
file_obj = FileUpload.objects.filter(upload_dir__icontains='auto_upload_file')
# フォームのボタンがクリックされるとファイルが作成される
if request.method == "POST":
pandas_csv()
# create_csv()
return redirect('app:index')
context = {
'todo_obj': todo_obj,
'file_obj': file_obj,
}
return render(request, 'app/index.html', context)
次に「index.html」にformタグを追記します。
<!-- app/templates/app/index.html -->
<!doctype html>
<html>
<head>
<title>テストアプリ</title>
</head>
<body>
<h2>Todoリスト</h2>
<br>
<form action="{% 'app:index' %}" method="POST">
{% csrf_token %}
<button type="submit">CSVファイルの作成</button>
</form>
....
....
</body>
</html>
アップロードされているファイルを削除したのち、トップページを表示してみます。
「CSVファイルの作成」ボタンをクリックしてみると
保存データからファイルを作成したCSVファイルのみ表示されれば成功です。
今回は簡易的な実装を試みましたが、ユーザー権限でファイルを隠したり、ファイルの中身を表示することが可能なので、余力のある方は試してみて下さい。
それでは以上となります。
最後までご覧いただきありがとうございました。