Python + Django でCSVのダウンロード／アップロード


Python + Django でデータベースのテーブルをCSV形式でダウンロードし、そのCSVを編集してアップロードし、データベースのテーブルを更新します

マスタの一括登録処理を作成するようなケースです。


以下の続きです。
[Python + DjangoをWindowsにインストールする](https://qiita.com/tiguchi919/items/f9052d259cec7fe54a00)
[Python + Django + psycopg2でposgreSQLに接続する]
(https://qiita.com/tiguchi919/items/0fd7bf799571330f0219)
[Python+Django+psycopg2で内部結合クエリを試す]
(https://qiita.com/tiguchi919/items/827865481e82bb32ad04)

※プロジェクト名やbase.htmlは少し変更しています。

###1.開発環境

OS=windows10 64bit home
python=3.6.5
Django=2.0.7
psycopg2=2.7.4

###2.概要

下図のような処理を実装します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/fa39def1-4233-fb6e-3961-825879bfedb9.png)

【解説】

(1)インポート

まず、テーブルの一覧画面で、①インポートボタンを押して、ファイル選択画面を表示し、②CSVを選択し
③送信ボタンを押すと、CSVのデータをテーブルに登録します。

(2)エクスポート

同様に、テーブル一覧画面で①エクスポートを押すと、テーブルを全件検索してCSVを作成して画面にダウンロードします。(1)で利用するCSVファイルとして利用するイメージです。



###3.アプリケーション作成


(1)アプリケーションを作成する

```
(venv) C:\data\python\myproject>python manage.py startapp csvdownload
```

(2)アプリの構成

以下、作成後のフォルダ、ファイル構成です。「#追加」は、startappでは作成されないので追加しています

```
csvdownload
│  admin.py
│  apps.py
│  forms.py  #追加
│  models.py
│  tests.py
│  urls.py  #追加
│  views.py
│  __init__.py
│  
・・・
├─templates  #追加
│  └─csvdownload  #追加
│          import.html  #追加
│          list.html  #追加
・・・
├─myproject
│  │  settings.py
│  │  urls.py
│  │  wsgi.py
・・・
├─static
│  ├─css
│  │      bootstrap.min.css
│  │      bootstrap.min.css.map
│  │      style.css
│  │      
│  └─js
│          bootstrap.bundle.min.js
│          jquery-3.3.1.min.js
│          
├─templates
・・・
│  ├─commons
│  │      base.html
```

###4.実装

(1)csvdownload\models.py

```csvdownload\models.py
from django.db import models

class Post(models.Model):
    """役職マスタ"""
    name = models.CharField('役職名', max_length=50)

    def __str__(self):
        return self.name
```

(2)myproject\setting.py

プロジェクトにアプリを追加します

```myproject\setting.py
・・・
INSTALLED_APPS = [
・・・・・
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'bootstrap4',
・・・・・
    'csvdownload', #追加
]
・・・
```

(3)migrate

migrateで、modelの追加と、データベース(postgreSQL)にもテーブルを追加しておきます。

```cmd.prompt
(venv) C:\data\python\myproject>python manage.py makemigrations csvdownload
(venv) C:\data\python\myproject>python manage.py migrate csvdownload
```

(4)csvdownload\forms.py

```csvdownload\forms.py
from django import forms

class CSVUploadForm(forms.Form):
    file = forms.FileField(label='CSVファイル')
```

(5)csvdownload\views.py

```csvdownload\views.py
import csv
import io
import urllib
from django.http import HttpResponse
from django.urls import reverse_lazy
from django.views import generic
from .forms import CSVUploadForm
from .models import Post

class Index(generic.ListView):
    """
    役職テーブルの一覧表作成
    """
    model = Post
    template_name = 'csvdownload/list.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['form_name'] = 'csvdownload'
        return ctx

class PostImport(generic.FormView):
    """
    役職テーブルの登録(csvアップロード)
    """
    template_name = 'csvdownload/import.html'
    success_url = reverse_lazy('csvdownload:index')
    form_class = CSVUploadForm

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['form_name'] = 'csvdownload'
        return ctx

    def form_valid(self, form):
        """postされたCSVファイルを読み込み、役職テーブルに登録します"""
        csvfile = io.TextIOWrapper(form.cleaned_data['file'])
        reader = csv.reader(csvfile)
        for row in reader:
            """
            役職テーブルを役職コード(primary key)で検索します
            """
            post, created = Post.objects.get_or_create(pk=row[0])
            post.name = row[1]
            post.save()
        return super().form_valid(form)

def PostExport(request):
    """
    役職テーブルを全件検索して、CSVファイルを作成してresponseに出力します。
    """
    response = HttpResponse(content_type='text/csv; charset=Shift-JIS')
    filename = urllib.parse.quote((u'CSVファイル.csv').encode("utf8"))
    response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'{}'.format(filename)
    writer = csv.writer(response)
    for post in Post.objects.all():
        writer.writerow([post.pk, post.name])
    return response

```

(6)csvdownload\urls.py

```csvdownload\urls.py
from django.urls import path
from . import views

app_name = 'csvdownload'
urlpatterns = [
    path('', views.Index.as_view(), name='index'),
    path('import/', views.PostImport.as_view(), name='import'),
    path('export/', views.PostExport, name='export'),
]
```

(7)csvdownload\templates\csvdownload\list.html

```list.html
{% extends 'commons/base.html' %}

{% block headertitle %}
CSVインポートエクスポート
{% endblock %}

{% block content %}
<div class="col-sm">
  <nav class="pull-right header">
      <ul id="menu">
        <li><a href="{% url 'csvdownload:import' %}" class="">インポート</a></li>
        <li><a href="{% url 'csvdownload:export' %}" class="">エクスポート</a></li>
      </ul>
  </nav>
</div>
<table class="table table-sm table-striped">
  <thead>
    <tr class="text-secondary bg-warning">
      <th class="" style="width:25%;">役職コード</th>
      <th class="">役職名</th>
    </tr>
  </thead>
  <tbody>
    {% for post in post_list %}
      <tr>
        <td>{{ post.pk }}</td>
        <td>{{ post.name }}</td>
      </tr>
    {% endfor %}
  </tbody>
</table>
{% endblock %}
```

(8)csvdownload\templates\csvdownload\import.html

```import.html
{% extends 'commons/base.html' %}

{% block headertitle %}
CSVインポート
{% endblock %}

{% block content %}
<form action="" method="POST" enctype="multipart/form-data">
  <p>ファイルを選択ボタンをクリックして、アップロードするCSVファイルを選択します</p>
  {% csrf_token %}
  <div class="form-group">
    {{form.file}}
  </div>
  <br/>
  <button type="submit">送信</button>
</form>
{% endblock %}
```

(9)templates\commons\base.html

再掲しておきます。

```templates\commons\base.html
{% load static %}
{% load bootstrap4 %}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="{% static 'css/bootstrap.min.css' %}">
    <link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}">
    <title>
      {% block title %}{% endblock %}
    </title>
    <style>
        body {
          padding : 3px;
        }
        div.container {
          min-width:600px;
        }
        nav.header ul#menu {
            margin:0px;
            padding:0px;
        }
        nav.header ul#menu li {
            float:right;
            list-style-type:none;
            text-decoration: underline;
            padding-left: 10px;
        }
        div.header {
          padding: 3px;margin-bottom: 5px;
        }
        div.header * {
          color: white !important;
        }
        div.header .title {
          font-size: 1.2em;
        }
        {% block styles %}{% endblock %}
    </style>
</head>
<body>
<div class="container">
  <div class="inner">
    <div class="text-right">{{ form_name }}</div>
    <div class="row header mysystem-header">
      <div class="col-sm title">
        {% block headertitle %}{% endblock %}
      </div>
      <div class="col-sm">
        <nav class="pull-right header">
            <ul id="menu">
              <li><a href="" onclick="javascript:history.go(-1); return false;">戻る</a></li>
              {% if user.is_authenticated %}
                <li><a href="{% url 'logout' %}" class="logout">ログアウト</a></li>
                {% if form_name == "password_change" %}
                {% else %}
                    <li><a href="{% url 'accounts:password_change' %}" class="">パスワード変更</a></li>
                {% endif %}
              {% else %}
              {% endif %}
              <li><a href="{% url 'accounts:index' %}" class="">ＴＯＰ</a></li>
            </ul>
        </nav>
      </div>
    </div>
    <div class="row content">
        {% block content %}{% endblock %}
    </div>
  </div>
</div>

<script src="{% static "js/jquery-3.3.1.min.js" %}"></script>
<script src="{% static "js/bootstrap.bundle.min.js" %}"></script>
{% block scripts %}{% endblock %}

</body>
</html>
```

###5.プロジェクトにアプリのURLを追加する

(1)myproject\urls.py

```myproject\urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
・・・・
    path('csvdownload/', include('csvdownload.urls')), #"追加")
    path('admin/', admin.site.urls),
]
```

###6.動作確認

(1)(venv) C:\data\python\myproject>manage.py runserver

[http://localhost:8000/csvdownload/](http://localhost:8000/csvdownload/)にアクセスします

![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/259e97ee-902c-feb3-2112-60de20189a05.png)

(2)エクスポート
エクスポートでCSVをダウンロードします
![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/eed12eec-8bb5-4a5e-64f3-7933cd4fa03d.png)

(3)CSV
ダウンロードしたファイルに1行追加して、インポートしてみます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/fd29df1b-f5b2-070f-0f3d-8d893ab81b98.png)


(4)インポート
編集したファイルを指定して送信します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/7e4bd0b5-e193-901e-89bd-2807346a3464.png)

(5)一覧画面で登録確認
更新に成功しています
![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/74c2694e-1187-7c83-20b9-f51f6ba94dd0.png)