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개로 늘어났음을 확인할 수 있다.
'프로그래밍 Programming' 카테고리의 다른 글
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 |