Django와 Handsontable.js를 이용한 엑셀풍의 입력화면 만들기
Django로 엑셀 풍의 입력 화면을 만들어보자. 이를 위해서 handsontable 을 이용한다. Handsontable은 엑셀의 외관을 가진 JavaScript/HTML5 스프레드시트 컴포넌트이다. 본 포스팅은 Hatena 블로거 thinkAmi님의 Django + Handsontable.jsを使って、Excel風な入力画面を作ってみた (http://thinkami.hatenablog.com/entry/2016/12/11/000415)을 참고로 작성되었다. 
     
https://github.com/handsontable/handsontable
1. 환경
본 포스팅의 환경은 다음과 같다. 
Ubuntu 14.04 + Python 3.5.2 + Django 1.9.8 + Handsontable 0.31.1 + js-cookie.js 2.1.3
목표는 아래와 같은 웹페이지를 만드는 것이다.
2. Handsontable installation 
1) Handsontable 을 설치한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26  | # npm install handsontable --save npm http GET https://registry.npmjs.org/handsontable npm http 200 https://registry.npmjs.org/handsontable npm http GET https://registry.npmjs.org/handsontable/-/handsontable-0.31.1.tgz npm http 200 https://registry.npmjs.org/handsontable/-/handsontable-0.31.1.tgz npm http GET https://registry.npmjs.org/pikaday npm http GET https://registry.npmjs.org/zeroclipboard npm http GET https://registry.npmjs.org/moment npm http GET https://registry.npmjs.org/numbro npm http 200 https://registry.npmjs.org/pikaday npm http GET https://registry.npmjs.org/pikaday/-/pikaday-1.5.1.tgz npm http 200 https://registry.npmjs.org/zeroclipboard npm http GET https://registry.npmjs.org/zeroclipboard/-/zeroclipboard-2.3.0.tgz npm http 200 https://registry.npmjs.org/numbro npm http GET https://registry.npmjs.org/numbro/-/numbro-1.9.3.tgz npm http 200 https://registry.npmjs.org/pikaday/-/pikaday-1.5.1.tgz npm http 200 https://registry.npmjs.org/zeroclipboard/-/zeroclipboard-2.3.0.tgz npm http 200 https://registry.npmjs.org/numbro/-/numbro-1.9.3.tgz npm http 200 https://registry.npmjs.org/moment npm http GET https://registry.npmjs.org/moment/-/moment-2.18.1.tgz npm http 200 https://registry.npmjs.org/moment/-/moment-2.18.1.tgz handsontable@0.31.1 node_modules/handsontable ├── zeroclipboard@2.3.0 ├── numbro@1.9.3 ├── pikaday@1.5.1 └── moment@2.18.1  | cs | 
2) handsontable 기본 사용법은 아래 링크 참조
http://www.antilibrary.org/1156
3. Handsontable 앱 생성
1) Django app을 생성한다.
1  | # python manage.py startapp handsontable  | cs | 
1 2 3 4 5 6 7 8 9 10 11 12  | # tree . ├── __init__.py ├── admin.py ├── apps.py ├── migrations │   └── __init__.py ├── models.py ├── tests.py └── views.py 1 directory, 7 files  | cs | 
2) 프로젝트 URL 설정
project/urls.py
1 2 3 4 5  | urlpatterns = [     url(r'^handsontable/', include('handsontable.urls', 'handsontable')), ]  | cs | 
3) 앱 URL 설정
handsontable/urls.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25  | from django.conf.urls import url from django.views.generic.base import TemplateView from django.views.generic.list import ListView from .views import HandsonTableView from .models import Header # http://qiita.com/irxground/items/cd83786b10d81eecce77 urlpatterns = [     url(r'^records/$',         ListView.as_view(model=Header),         name='record-index',     ),     url(r'^records/new$',          TemplateView.as_view(template_name='handsontable/detail.html'),         name='record-new'     ),     url(r'^records/(?P<pk>[0-9]+)/edit$',          TemplateView.as_view(template_name='handsontable/detail.html'),         name='record-edit'     ),     url(r'^ajax/records/(?P<pk>[0-9]+)$',         HandsonTableView.as_view(),         name='ajax',     ), ]  | cs | 
4) 예제에 사용할 모델을 만든다. 본 예제는 다음과 같이 Header와 Detail 이상 2개의 테이블로 구성된다. Header에서는 해당 품목의 id와 생성일자의 컬럼으로 구성되고, Detail 테이블에서 해당 품목(id)의 구매내역(구매자명, 일시, 구매가격)을 별도로 관리한다. 
handsontable/models.py
1 2 3 4 5 6 7 8 9 10 11  | from django.db import models # Create your models here. class Header(models.Model):     update_at = models.DateTimeField('UpdateAt') class Detail(models.Model):     header = models.ForeignKey(Header)     purchase_date = models.DateField('Date')     name = models.CharField('Name', max_length=255)     price = models.DecimalField('Price', max_digits=10, decimal_places=0)  | cs | 
5) 마이그레이션을 실시한다. 
1 2 3 4 5 6 7  | # python manage.py makemigrations Migrations for 'handsontable':   0001_initial.py:     - Create model Detail     - Create model Header     - Add field header to detail  | cs | 
1 2 3 4 5 6 7  | # python manage.py migrate Operations to perform:   Apply all migrations: handsontable Running migrations:   Rendering model states... DONE   Applying handsontable.0001_initial... OK  | cs | 
4. View 생성
리스트, 신규작성, 그리고 편집용 View 를 만든다.
handsontable/views.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52  | import json from django.views.generic import View, ListView from django.http import HttpResponse from django.core import serializers from django.utils import timezone from .models import Header, Detail NEW_PAGE_ID = 0 class HandsonTableView(View):     def get(self, request, *args, **kwargs):         details = Detail.objects.filter(header__pk=self.kwargs.get('pk')).select_related().all()         return HttpResponse(             serializers.serialize('handsontablejson', details),             content_type='application/json'         )     def post(self, request, *args, **kwargs):         body_unicode = request.body.decode('utf-8')         body = json.loads(body_unicode)         header = self.update_header(self.kwargs.get('pk'))         Detail.objects.filter(header=header).delete()         for b in body:             Detail(                 header=header,                 purchase_date=b.get('purchase_date'),                 name = b.get('name'),                 price = b.get('price'),             ).save()         return HttpResponse('OK')     def update_header(self, pk):         if int(pk) == NEW_PAGE_ID:             new_header = Header(update_at = timezone.now())             new_header.save()             print(Header.objects.latest('id').id)             return new_header         header = Header.objects.filter(pk=pk).first()         header.update_at = timezone.now()         header.save()         return header  | cs | 
5. Serialization 모듈 설정
앞선 views.py의 get() 메서드에는 Model을 JSON화 시키고, JSON을 응답으로 돌려보내는 장치가 마련되어 있다. 프로젝트에 serializers 파일을 만들고 이를 settings.py 에 등록한다.
project/serializers.py
1 2 3 4 5 6 7 8 9 10 11  | from django.core.serializers.json import Serializer class Serializer(Serializer):     def get_dump_object(self, obj):         return self._current     def start_serialization(self):         super(Serializer, self).start_serialization()         self.json_kwargs["ensure_ascii"] = False         self.json_kwargs['indent'] = 2  | cs | 
settings.py
1 2 3  | SERIALIZATION_MODULES = {     "handsontablejson": "vikander.serializers" }  | cs | 
6. 템플릿 파일 생성
아래와 같이 2개의 템플릿을 만든다.
<Project Name>/<App Name>/templates/handsontable/header_list.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22  | <div class="card-block">     <p class="deck">         <a href="{% url 'handsontable:record-new' %}">신규등록</a>     </p>     <p>         <table class="table">             <tr>                 <th><th>                 <th>갱신일시</th>                 <th>조작</th>             </tr>             {% for record in object_list %}                 <tr>                     <td></tD>                     <td>{{ record.id }} </td>                     <td>{{ record.update_at }}</td>                     <td><a href="{% url 'handsontable:record-edit' record.id %}">편집</a></td>                 </tr>             {% endfor %}         </table>         </p> </div>  | cs | 
<Project Name>/<App Name>/templates/handsontable/detail.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16  | {% load static %} <head>     <link href="{% static 'assets/css/handsontable.full.min.css' %}" rel="stylesheet"> </head> <div class="card-block">     <p class="deck">         <div id="grid"></div>         <button type="button" id="add">Add</button>         <button type="button" id="save">Save</button>         <a href="{% url 'handsontable:record-index' %}">List</a>         <script src="{% static 'assets/js/libs/handsontable.full.min.js' %}"></script>         <script src="{% static 'assets/js/libs/js.cookie-2.1.3.min.js' %}"></script>         <script src="{% static 'assets/js/libs/myscript.js' %}"></script>     </p> </div>  | cs | 
7. Handsontable 오브젝트 설정
/static/assets/myscript.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83  | const NEW_PAGE_ID = 0; var data = []; var grid = document.getElementById('grid'); var table = new Handsontable(grid, {     data: data,     columns: [         { data: 'purchase_date', type: 'text' },         { data: 'name', type: 'text' },         { data: 'price', type: 'numeric' },     ],     colHeaders: ["Date", "Name", "Price"],     rowHeaders: true,     colWidths: [120, 200, 100] }); var id = (() => {     var found = location.pathname.match(/\/handsontable\/records\/(.*?)\/edit$/);     return found ? found[1] : NEW_PAGE_ID; })(); Handsontable.hooks.add('onAddRow', mydata => {     table.alter('insert_row', data.length); }); Handsontable.hooks.add('onSave', mydata => {     var csrftoken = Cookies.get('csrftoken');     fetch(`/handsontable/ajax/records/${id}`, {         method: 'POST',         headers: {             'content-type': 'application/json',             'X-CSRFToken': csrftoken         },         mode: 'same-origin',         credentials: 'same-origin',         body: JSON.stringify(mydata),     }).then(response => {         console.log(response.url, response.type, response.status);         if (response.status == '200'){             window.alert('Saved');             location.href = '/handsontable/records';         }         else{             window.alert('Failed');         }     }).catch(err => console.error(err)); }); document.addEventListener("DOMContentLoaded", () => {     fetch(`/handsontable/ajax/records/${id}`, {         method: 'GET',     }).then(response => {         console.log(response.url, response.type, response.status);         response.json().then(json => {             for (var i = 0; i < json.length; i++){                 data.push({                     purchase_date: json[i].purchase_date,                     name: json[i].name,                     price: json[i].price,                 });             }             table.render();         });     }).catch(err => console.error(err)); }, false); document.getElementById('save').addEventListener('click', () => {     Handsontable.hooks.run(table, 'onSave', data); }); document.getElementById('add').addEventListener('click', () => {     Handsontable.hooks.run(table, 'onAddRow', data); });  | cs | 
/static/assets/js/libs/js.cookie-2.1.3.min.js
https://github.com/js-cookie/js-cookie/releases
/static/assets/js/libs/handsontable.full.min.js
https://github.com/handsontable/handsontable/releases
/static/assets/css/handsontable.full.min.css
https://github.com/handsontable/handsontable/releases
완성된 모습이다.
신규등록을 통해 품목을 새롭게 등록할 수 있다. 신규등록 클릭시 세부 판매 내역이 비어있는 detail.html 로 이동하게 된다. [Add] 버튼을 눌러서 행을 추가하고 데이터를 입력한다. 
데이터 입력이 끝나고 [Save] 를 누르면 아래와 같이 저장에 성공했다는 메시지가 뜬다. 
데이터베이스에 다음과 같이 저장이 되었다. 
품목이 3개로 늘어났음을 확인할 수 있다.
'Season 1 아카이브 > 프로그래밍' 카테고리의 다른 글
| conda vs. pip vs. virtualenv (0) | 2017.04.13 | 
|---|---|
| 장고 사이트에 reCAPTCHA 넣기 Add reCAPTCHA to Django site (0) | 2017.04.10 | 
| Handsontable 기본 사용법 (0) | 2017.03.25 | 
| Django Channels로 채팅룸 만들기 (0) | 2017.02.21 | 
| PIP로 특정 버전 패키지 설치하는 법 To install a specific version of a package with pip (0) | 2017.02.20 |