DjangoでCSVアップロード、データ処理、ダウンロード機能を作成してみた

##やりたいこと
CSVを複数取り込み、処理をしてから結果を同じくCSVで返す。
今回は以下のケースを使用してこれを実装する。

##ケース
ある大学で、以下のCSVが入手できている。
`available.csv`では、各クラスのキャパシティがどのくらいかの情報を持つ。
`reserved.csv`では、各クラスで何人の生徒が授業を履修予定かの情報を持つ。
今回、これらのファイルをブラウザ上でアップロードすると、現在どのくらい各クラスに空きがあるかを示すCSVをダウンロードする様なアプリを作成する。

##設計
フロント側では、主に以下の２つの動きがある。
①：ファイルアップロード
②：アップロードされたファイルを元に各クラスの空きを示すCSVを吐き出す

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464595/0c1ebe1f-b744-5661-cffa-c186883b68a7.png)

##前提
Djangoのプロジェクトフォルダ(`myproject`)とアプリフォルダ(`app`)が既にある前提とする。ない場合は[こちら](https://docs.djangoproject.com/en/3.0/intro/tutorial01/)を参照。

##フォルダ構成

今回、以下のようなフォルダ構成を取る。

```
├─app
│  │  admin.py
│  │  apps.py
│  │  forms.py
│  │  functions.py
│  │  models.py
│  │  tests.py
│  │  views.py
│  │  __init__.py
│  │
│  ├─migrations
│  │    __init__.py
│  │
│  ├─static
│  │  └─upload
│  │          test.txt
│  │
│  └─templates
│       index.html
│
├─myproject
│  │  settings.py
│  │  urls.py
│  │  wsgi.py
│  │  __init__.py
│  │
│  └─static
│        humans.txt
│
└─staticfiles
```

##フォーム作成
まず、ファイルをアップロードできるよう、以下のように`forms.py`でフォームを定義する。

```python
from django import forms
#ClassroomFormを定義
class ClassroomForm(forms.Form):    
    availablity = forms.FileField() 
    reservation = forms.FileField()
```
##フォームをHTMLへ渡す
次に、作成したフォームをフロント側で表示するため、以下のように`views.py`でフォームをHTMLに渡す。

```python
from django.shortcuts import render  
from app.forms import ClassroomForm  

def index(request): 
    ###ClassroomFormをindex.htmlに渡す
    classroom = ClassroomForm()  
    return render(request,"index.html",{'form':classroom}) 
```

これで`index.html`で`{{ form.as_p }} `を使用し、以下のようにフォームを引っ張ってこれる。

```html
<body>  
    <form method="POST" class="post-form" enctype="multipart/form-data">  
            {% csrf_token %}  
            {{ form.as_p }}  
            <button type="submit" class="save btn btn-default">Save</button>  
    </form>  
</body> 
```

##POST処理
このままフォームを提出すると、`views.py`の`index`メゾットにPOSTされた情報が渡される。なので、POST処理をするよう`index`を以下のように書き換える。

```python
from django.shortcuts import render  
from django.http import HttpResponse  
from app.functions import process_files
from app.functions import write_into_csv
from app.forms import ClassroomForm  
from django.template import loader
import csv

def index(request):  
    if request.method == 'POST':  
        classroom = ClassroomForm(request.POST, request.FILES)
        #classroomにデータがある場合
        if classroom.is_valid():  
            availability = request.FILES['availablity']
            reservation = request.FILES['reservation']
            #implement process_files
            csv_data = process_files(availability, reservation)
            #download result as a csv format             
            response = write_into_csv(csv_data)

            return response
    else:       
        ###ClassroomFormをindex.htmlに渡す
        classroom = ClassroomForm()  
        
        return render(request,"index.html",{'form':classroom})  
```

ここでは、`functions.py`で定義された`process_files`と`write_into_csv`という２つのメゾットを呼び出している。次でこのメゾットを定義する。

##ロジックを定義
`functions.py`にて`views.py`で使用するロジックを定義する。

```Python
import csv
from django.http import HttpResponse  

### Process csv files
def process_files(availability, reservation):  
    
        """ Description
        :type availability:
        :param availability:
    
        :type reservation:
        :param reservation:
    
        :raises:
    
        :rtype:
        """
        availability_dict = convert_to_dict(availability)
        reservation_dict = convert_to_dict(reservation)
    
        csv_data = []

        courses = list(availability_dict)
    
        for course in courses:
            remaining = availability_dict[course] - reservation_dict[course]
            row = [course, remaining]
            csv_data.append(row)
        return csv_data

def convert_to_dict(file):
    
        """ Description
        :type file:
        :param file:
    
        :raises:
    
        :rtype:
        """
        data = file.read().decode("utf-8-sig")
        lines = data.split("\r\n")
        dict = {}
        for line in lines:	
            fields = line.split(",")
            course = fields[0]
            capacity = int(fields[1])
            dict[course] = capacity

        return dict


def write_into_csv(csv_data):
        """ Description
        :type csv_data:
        :param csv_data:
    
        :raises:
    
        :rtype:
        """
        response = HttpResponse(content_type='text/csv')
        response['Content-Disposition'] = 'attachment; filename="download.csv"'
        writer = csv.writer(response)  

        for row in csv_data:
            writer.writerow(row)

        return response
```

以上で設計通りの機能を実装できた。

##Demo
![File_Upload_Demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464595/eb659773-976b-f4d0-c9bd-775bb2916666.gif)

##最後に
参考になったらぜひLikeをしてください。

##Github
https://github.com/norifumi92/csv_uploader/tree/develop


##参考
https://www.javatpoint.com/django-file-upload
https://codepen.io/adamlaki/pen/VYpewx
https://www.pythoncircle.com/post/30/how-to-upload-and-process-the-csv-file-in-django/
https://into-the-program.com/customize-input-type-file/
https://docs.djangoproject.com/en/3.0/topics/forms/