概要
本記事ではReactとDjangoを使用して帳票(PDF)作成する機能と、
作成した帳票を閲覧できる機能を実装します。
djangorestframeworkを使用してReactとAPI通信を行います。
PDFの作成にはreportlabライブラリを使用します。
以下、バージョン情報など。
OS : MacOS(M1)
DB : SQLite3
Django==4.2.9
django-cors-headers==4.3.1
djangorestframework==3.14.0
reportlab==4.0.8
react@18.2.0
react-router-dom@6.21.1
環境構築
環境構築は特に解説など交えずさらっといきます。
Djangoの開発環境構築
python3 -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
pip install django
pip install django-cors-headers
pip install djangorestframework
pip install reportlab
pip freeze > requirements.txt
django-admin startproject core .
python manage.py startapp pdfapp
- django-cors-headersは、Reactからのリクエストがブロックされるのを防ぎ、CORSヘッダーを設定するのに必要
Djangoの設定ファイルを修正
以下、修正した箇所です。
import os
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'pdfapp', #追加
'rest_framework',#追加
'corsheaders',#追加
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware', #追加
]
TIME_ZONE = 'Asia/Tokyo'
LANGUAGE_CODE = 'ja'
CORS_ORIGIN_WHITELIST = [
'http://localhost:3000', # ReactアプリのURL
]
# ↓PDFを保存する場所
# mediaディレクトリはmanage.pyがあるディレクトリで作成
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
DjangoプロジェクトのURLを修正
以下がURL全体のコードです。
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path, include
urlpatterns = [
path('api/', include('pdfapp.urls')),
path('admin/', admin.site.urls),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
- 今回DjangoはAPIを受け付ける用なので、api/できたリクエストはpdfappのurlsを参照するように設定
- +staticの部分はローカル環境で作成したPDFをWEBで見れるようにするのに必要
pdfappに必要なファイルを作成
- serializers.py
- urls.py
上記の2つのファイルをDjangoアプリ内で作成しておいてください。
Reactプロジェクトの作成とライブラリのインストール
DjangoのプロジェクトルートディレクトリでReactアプリを作成し必要なライブラリをインストールしましょう。
npx create-react-app myapp
cd myapp
npm install react-router-dom
npm install axios
- react-router-domはルーティングするのに必要なライブラリです
- axiosは、JavaScriptでHTTPリクエストを行うためのライブラリです
APIの転送設定
APIリクエストをDjangoサーバーに転送できるようにmyapp ディレクトリ内のpackage.jsonにプロキシ設定を追加します
{
"name": "myapp",
----省略----
"browserslist": {
----省略----
},
"proxy": "http://localhost:8000"
}
現状のディレクトリ構成を2階層まではっ付けておきます。
.
├── core
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── media
├── myapp
│ ├── README.md
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ └── src
├── package-lock.json
├── package.json
├── pdfapp
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ ├── models.py
│ ├── serializers.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── requirements.txt
└── venv
├── bin
├── include
├── lib
└── pyvenv.cfg
モデル作成
今回はサンプルとして製品モデルを作成します。
モデルの定義については各人の要件に合わせて修正してください。
モデルを作成したらpython manage.py makemigrationsとpython manage.py migrateしてください。
from django.db import models
class Product(models.Model):
product_name = models.CharField(max_length=255)
description = models.TextField()
pdf_file = models.FileField(upload_to='pdf/', null=True, blank=True)
class Meta:
db_table = 'M_Products'
- 先ほど作成したmedia/ディレクトリ内にpdfディレクトリを作成しておいてください
- media/pdfにPDFが保存されていくようにします
シリアライザーの作成
API通信でデータの送受信をするため、シリアライザーをDjangoアプリ内に作成しましょう。
from rest_framework import serializers
from .models import Product
class ProductModelSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'
- シリアライザは、モデルのデータをJSON形式などの形式に変換するために使用され、またその逆の変換も行います。これにより、DjangoのモデルをRESTfulなAPIを通じてやり取りすることが可能になります
- serializers.ModelSerializerはDjangoのモデルを自動的にシリアライズするための機能を提供します
- model = でこのシリアライザがどのモデルに対して使用されるかを指定
- 全てのフィールドをシリアライズに含めることを指定
viewsの作成
次にviewsを作成しましょう。
def create_pdf(product_instance):
# PDFファイルの名前とパスを設定
pdf_name = f"product_{product_instance.id}.pdf"
pdf_path = os.path.join(settings.MEDIA_ROOT, 'pdf', pdf_name)
c = canvas.Canvas(pdf_path)
# 各テキストの開始位置
start_x = 100
start_y = 800
line_height = 20
c.drawString(start_x, start_y, f"プロダクト名: {product_instance.product_name}")
c.drawString(start_x, start_y - line_height * 1, f"説明: {product_instance.description}")
c.save()
return pdf_name, pdf_path
class ProductModelViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductModelSerializer
def perform_create(self, serializer):
instance = serializer.save()
pdf_name, pdf_path = create_pdf(instance)
# PDFファイルをモデルのpdf_fileフィールドに保存
with open(pdf_path, 'rb') as pdf_file:
instance.pdf_file.save(pdf_name, File(pdf_file), save=True)
instance.save()
◎create_pdfについて
- create_pdfは、与えられたproduct_instance(Productモデルのインスタンス)に基づいてPDFファイルを作成し、そのファイル名とパスを返す関数です
- start_xとstart_yはPDF内のテキストの開始位置を指定し、line_heightは行間の高さを指定
- 最後にsaveメソッドを呼び出してPDFファイルを保存し、ファイル名とパスを返します
◎ProductModelViewSetについて
- ProductModel対するCRUD操作を提供するAPIエンドポイントを作成
- querysetとserializer_classは操作するモデルとシリアライザを指定
- perform_createは、新しいProductModelインスタンスが作成された後に呼び出されます
- このメソッドはcreate_pdf関数を使用してPDFを作成し、モデルのpdf_fileフィールドに保存する役割があります
- instance.save()が呼び出されて初めてデータ(PDFファイルがpdf_fileに保存される)はモデルに保存されます
アプリURLの設定
pdfappのurls.pyを修正します
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ProductModelViewSet
router = DefaultRouter()
router.register(r'product', ProductModelViewSet)
urlpatterns = [
path('', include(router.urls)),
]
- Django REST frameworkのDefaultRouterを使用して、ProductModelViewSetに対するURLルートを自動的に作成しています
- DefaultRouterのインスタンスを作成し、ProductModelViewSetをproductというURLパスに登録
- これにより、ProductModelViewSetが提供するAPIエンドポイント(CRUD操作)に対するURLが自動的に生成される
- これらの設定により、例えばProductモデルのリストを取得するためのGETリクエストは/product/に対して行われ、新しいProductインスタンスを作成するためのPOSTリクエストも同じパスに対して行われます
- 特定のProductインスタンスに対する操作(取得、更新、削除)は、インスタンスのIDをURLに含めることで行うことができます(例:/product/1/)
React
まずは帳票作成と関係ない部分ですが、WEBページのデザインなどを実装します。
ここについては解説しません。
デザインなどWEBページの基本部分を作成
src/ディレクトリにcomponentsディレクトリを作成し、このディレクトリにコンポーネントファイルとCSSを定義します。
※本記事でメイン解説するのはProductCreate.jsxとProductList.jsxです。
作成するファイルは以下の通り。
- Header.jsx
- Footer.jsx
- Sidebar.jsx
- MainContent.jsx
- Home.jsx
- ProductCreate.jsx
- ProductList.jsx
- css/style.css
各ファイルの内容を添付しておきます。
ちなみに、デザインはAdminLTEを使用しています。(MITライセンスです)
<!-- AdminLTE CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@3.1/dist/css/adminlte.min.css">
<!-- FontAwsome -->
<link href="https://use.fontawesome.com/releases/v6.2.0/css/all.css" rel="stylesheet">
<title>帳票作成アプリ</title>
<!-- AdminLTE JS(bodyタグ最後部に追加) -->
<script src="https://cdn.jsdelivr.net/npm/admin-lte@3.1/dist/js/adminlte.min.js"></script>
- head内にCSSなど追加
- body内にスクリプト追加
.userName {
color: white;
}
.navbar-nav {
height: 40px;
}
.content-wrapper {
padding: 20px;
}
.customCard {
padding: 30px;
border: none;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.05);
}
.custom-file {
width: auto;
}
.customBtn {
width: auto;
float: right;
min-width: 200px;
margin-right: 30px;
}
.customMark {
margin-left: 10px;
color: red;
font-size: 20px;
vertical-align: middle;
}
import React from 'react';
import './css/style.css';
function Header() {
return (
<nav className="main-header navbar navbar-expand navbar-white navbar-light">
<ul className="navbar-nav">
</ul>
</nav>
)
}
export default Header;
import React from 'react';
import './css/style.css';
function Footer() {
return (
<footer className="main-footer">
<div className="float-right d-none d-sm-block">
<b>Version</b> 1.0.0
</div>
<strong>Copyright © MJ.</strong> All rights reserved.
</footer>
)
}
export default Footer;
import React from 'react';
import { Link } from 'react-router-dom';
import './css/style.css';
function Sidebar() {
return (
<aside className="main-sidebar sidebar-dark-primary elevation-4">
<div className="brand-link">
<img src={process.env.PUBLIC_URL + '/logo512.png'} alt="Logo" className="brand-image img-circle elevation-3" style={{ opacity: 0.8 }} />
<span className="brand-text font-weight-light">帳票作成アプリ</span>
</div>
<div className="sidebar">
<div className="user-panel mt-3 d-flex">
<div className="image">
<img src={process.env.PUBLIC_URL + '/images/userlogo.png'} className="img-circle elevation-2" alt="User Image" />
</div>
<div className="info">
<p className="d-block userName">User Name 1</p>
</div>
</div>
<nav className="mt-2">
<ul className="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li className="nav-item">
<Link to="/component1" className="nav-link">
<i className="nav-icon fas fa-house"></i>
<p>Home</p>
</Link>
</li>
<li className="nav-item">
<Link to="/component2" className="nav-link">
<i className="nav-icon fas fa-file-pdf"></i>
<p>帳票作成</p>
</Link>
</li>
<li className="nav-item">
<Link to="/component3" className="nav-link">
<i className="nav-icon fas fa-book"></i>
<p>帳票一覧</p>
</Link>
</li>
</ul>
</nav>
</div>
</aside>
);
}
export default Sidebar;
import React from 'react';
// Components
import Home from './Home';
import ProductCreate from './ProductCreate';
import ProductList from './ProductList';
function MainContent({ selectedComponent }) {
switch (selectedComponent) {
case 'component1':
return <Home />;
case 'component2':
return <ProductCreate />;
case 'component3':
return <ProductList />;
default:
return <Home />;
}
}
export default MainContent;
import React from 'react';
import './css/style.css';
function Home() {
return (
<div className="content-wrapper">
<section className="content-header">
<div className="container-fluid">
<div className="row mb-2">
<div className="col-sm-6">
<h1>Title</h1>
</div>
</div>
</div>
</section>
<section className="content">
<div className="container-fluid">
<p>hello world</p>
</div>
</section>
</div>
)
}
export default Home;
import React from 'react';
import './css/style.css';
function ProductCreate() {
return (
<p>create</p>
)
}
export default ProductCreate;
import React from 'react';
import './css/style.css';
function ProductList() {
return (
<p>list</p>
)
}
export default ProductList;
import React, { useState } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// Components
import Header from './components/Header';
import Sidebar from './components/Sidebar';
import MainContent from './components/MainContent';
import Footer from './components/Footer';
function App() {
return (
<Router>
<div className="wrapper">
<Header />
<Sidebar />
<Routes>
<Route path="/" element={<MainContent />} />
<Route path="/component1" element={<MainContent selectedComponent="component1" />} />
<Route path="/component2" element={<MainContent selectedComponent="component2" />} />
<Route path="/component3" element={<MainContent selectedComponent="component3" />} />
</Routes>
<Footer />
</div>
</Router>
);
}
export default App;
PDF作成機能実装
ProductCreate.jsxの中身は以下です。
import React, { useState } from 'react';
import axios from 'axios';
import './css/style.css';
function ProductCreate() {
const [formData, setFormData] = useState({
productName: '',
description: '',
});
const handleInputChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (event) => {
event.preventDefault();
const dataToSend = new FormData();
dataToSend.append('product_name', formData.productName);
dataToSend.append('description', formData.description);
try {
const response = await axios.post('http://localhost:8000/api/product/', dataToSend, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
console.log('送信成功:', response.data);
alert('帳票作成に成功しました。');
// フォームのリセット
setFormData({
productName: '',
description: '',
});
} catch (error) {
console.error('送信エラー:', error);
alert('送信に失敗しました。');
}
};
return (
<div className="content-wrapper">
<section className="content-header">
<div className="container-fluid">
<div className="row mb-2">
<div className="col-sm-6">
<h1><strong>帳票作成</strong></h1>
</div>
</div>
</div>
</section>
<section className="content">
<div className="container-fluid">
<div className="card customCard">
<form onSubmit={handleSubmit}>
{/* プロダクト名 */}
<div className="row col-12 mb-3">
<div className="col-lg-3 col-sm12 mb-3">
<p><strong>プロダクト名</strong><strong className="customMark">*</strong></p>
</div>
<div className="col-lg-9 col-sm12">
<input
className="form-control"
type="text"
placeholder=""
name="productName"
value={formData.productName}
onChange={handleInputChange}
required
/>
</div>
</div>
{/* 説明 */}
<div className="row col-12 mb-3">
<div className="col-lg-3 col-sm12 mb-3">
<p><strong>説明</strong><strong className="customMark">*</strong></p>
</div>
<div className="col-lg-9 col-sm12">
<textarea
className="form-control"
rows="5"
placeholder=""
name="description"
value={formData.description}
onChange={handleInputChange}
></textarea>
</div>
</div>
<button type="submit" className="btn btn-block btn-success customBtn">帳票を作成する</button>
</form>
</div>
</div>
</section>
</div>
)
}
export default ProductCreate;
- axiosライブラリをインポートして、非同期HTTPリクエストを行います
- useStateフックを使用して、フォームのデータ(formData)の状態管理をする
- handleInputChange関数は、フォームの入力フィールドが変更されたときに呼び出されます。この関数はformData状態を更新します
- handleSubmit関数は、フォームの送信ボタンが押されたときに実行されます
- axios.postを使用して、APIエンドポイントにデータを送信します
PDFの一覧機能
以下がjsxの中身です。
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './css/style.css';
function ProductList() {
const [ledgerItems, setLedgerItems] = useState([]);
useEffect(() => {
axios.get('http://localhost:8000/api/product/')
.then(response => {
setLedgerItems(response.data);
})
.catch(error => {
console.error('データの取得に失敗しました:', error);
});
}, []);
return (
<div className="content-wrapper">
<section className="content-header">
<div className="container-fluid">
<div className="row mb-2">
<div className="col-sm-6">
<h1><strong>帳票一覧</strong></h1>
</div>
</div>
</div>
</section>
<section className="content">
<div className="container-fluid">
<div>
{ledgerItems.map(item => (
<div className="card customCard" key={item.id}>
<div className="row col-lg-12">
<div className="row col-lg-6 col-sm-12">
{/* プロダクト名 */}
<div className="row listInfo">
<div className="col-12">
<p><strong>プロダクト名</strong></p>
</div>
<div className="col-12">
<p>{item.product_name}</p>
</div>
</div>
</div>
<div className="col-lg-6 col-sm-12">
<a href={item.pdf_file} target="_blank" rel="noopener noreferrer">PDFを見る</a>
</div>
</div>
</div>
))}
</div>
</div>
</section>
</div>
)
}
export default ProductList;
- axios.getを使用して、指定されたAPIエンドポイント(http://localhost:8000/api/product/)から製品データを取得
- レスポンスが成功した場合、setLedgerItemsを使用してledgerItems状態を更新
- ledgerItems配列を.mapメソッドを使用して反復処理し、各製品のデータを表示
これでPDFの作成と一覧を見ることができるようになりました。