Python | Django | React | Todo webアプリの作成方法
Pythonには,DjangoというWebアプリケーションフレームワークがある.フレームワークのため,Djangoを利用するとWebアプリを通常よりも短時間で開発することが可能になる.
Reactとは,コンポーネンツをベースとしたユーザーインターフェイスを構築するためのフリーのフロントエンドJavaScriptライブラリーであり,Meta社によって開発された.
Reactは,Next.jsのようなフレームワークを利用してシングルページやモバイルもしくはサーバーレンダーアプリケーションの開発に利用することができる.
本記事では,「Todo webアプリの作成方法」を以下に記す.作成の際,DjangoプロジェクトとReactプロジェクトは別々に開発する.
なお,作成したTodo webアプリのデータベースに"SQLite"ではなく"MariaDB"を利用したい場合は,以下記事が参考になる.
shelokuma tech blog | 作成したwebアプリのデータベースをSQLiteからMariaDBに変更する方法
実施環境
Windows 11
Python 3.11.1
Django 5.1.1
Visual Studio Code (VS Code) 1.93.1
shelokuma tech blog | バージョン確認方法
完成したTodo webアプリ
これから作るTodo webアプリの完成品が以下になる.
Todo webアプリの作成方法
Djangoでの開発
ディレクトリを作成し,Visual Studio Code (VS Code)を起動させる.
以下画面が表示されるので,"File"をクリックし,"Open Folder"を選択し,上記で作成したディレクトリを選択する.
選択後,以下画面になるので,"Teminal"を開く.開くには以下2つのやり方がある.
- Ctrl + Shift + @ボタンを同時押し
- “…"をクリックし,"Terminal"を選択,"New Terminal"をクリックする.
以下画面になる.現在は"241001_todoApp"ディレクトリにいる.
以下コマンドを実行し,"Pipenv"をインストールする.
$ pip install pipenv
Django用に"backend"ディレクトリを作成し,当該ディレクトリに移動する.
$ mkdir backend
$ cd backend
“backend"ディレクトリ下にて,以下コマンドを実行する.仮想環境が作られ,django, djangorestframeworkがインストールされる.
$ pipenv install django djangorestframework
“backend"ディレクトリに,"Pipfile"や"Pipfile.lock"が以下のように作られれば成功となる.
以下コマンドを実行し,pipenv環境をアクティベートする
$ pipenv shell
以下コマンドを実行し,django projectを作成する.
※最後のdot(.)を忘れないこと.dotがないと,"todoproject"の下に"todoproject"が作成される.
$ django-admin startproject todoproject .
続けて以下コマンドを実行し,django appを作成する.
$ python manage.py startapp api
上記実行後,以下のようなディレクトリ構成となる.
※"todoproject"ディレクトリは1つしかないことに注意する.
backend
├─api
│ └─migrations
└─todoproject
“todoproject/settings.py"内の"INSTALLED_APPS"に,以下"rest_framework", “api"を追加する.
INSTALLED_APPS = [
...
'rest_framework',
'api',
]
“api/models.py"を以下のように更新する.Todo modelをtitle, completed, attachmentにて定義する.
from django.db import models
class Todo(models.Model):
title = models.CharField(max_length=100)
completed = models.BooleanField(default=False)
attachment = models.FileField(upload_to='uploads/', blank=True, null=True) # Optional file attachment
def __str__(self):
return self.title
Terminalに戻り,以下コマンドを実行する.
$ python manage.py makemigrations
その後,以下コマンドを実行する.
$ python manage.py migrate
“api"ディレクトリに"serializers.py"ファイルを作成する.作成後,以下構成となる.
“api/serializers.py"を以下のように更新する.JSON dataとmodelを紐づけるのに利用される.
from rest_framework import serializers
from .models import Todo
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = ['id', 'title', 'completed', 'attachment'] # Include necessary fields
“api/views.py"を以下のように更新する.Todo modelのAPIリクエストを操作するのに"TodoViewSet"を作成した.
from rest_framework import viewsets
from .models import Todo
from .serializers import TodoSerializer
class TodoViewSet(viewsets.ModelViewSet):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
“api"ディレクトリに"urls.py"を作成し,"api/urls.py"を以下のように更新する.
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TodoViewSet
router = DefaultRouter()
router.register('todos', TodoViewSet, basename='todo')
urlpatterns = [
path('', include(router.urls)),
]
“todoproject/urls.py"を以下のように更新する.pathの設定とtodo listに添付したファイルを閲覧するのに利用される.
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
“todoproject/settings.py"に以下コードを追加する."import os"は"from pathlib import Path"の上に追加し,"MEDIA_URL"と"MEDIA_ROOT"は最下部に追加した.
import os
...
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
Terminalにて以下コマンドを実行する.以下メッセージが出力されるので,"http://127.0.0.1:8000/"をクリックすると,ブラウザが開く.
$ python manage.py runserver
# 以下メッセージが出力
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
October 03, 2024 - 13:57:44
Django version 5.1.1, using settings 'todoproject.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
ブラウザにて,"http://127.0.0.1:8000/api/todos/"にアクセスし,以下画面が表示されれば成功となる.
こちらの画面からTodoを追加することもできる.
別々に開発するため,CORSの対処
今回,DjangoとReactを別々に開発し,別々のポートでアプリを走らせる.つまり,Djangoはport 8000, Reactはport 3000なので,CORS (Cross-Origin Resource Sharing) 問題が発生する.そのため,以下を実行していく.
“backend"ディレクトリにて,以下コマンドを実行する.
$ pipenv install django-cors-headers
“todoproject/settings.py"内の"INSTALLED_APPS"に"corsheaders"を追加する.
INSTALLED_APPS = [
...,
'corsheaders',
]
“todoproject/settings.py"内の"MIDDLEWARE"に"corsheaders.middleware.CorsMiddleware"を追加する.
MIDDLEWARE = [
...,
'corsheaders.middleware.CorsMiddleware',
]
“todoproject/settings.py"内に以下コードを追加する.
CORS_ALLOWED_ORIGINS = [
'http://localhost:3000',
]
Reactでの開発
新たにVS Codeを開き,上記で作成した"241001_todoApp"ディレクトリにて,以下コマンドを実行し,"frontend"ディレクトリを作成する.今後は"frontend"ディレクトリ内で開発を進めていく.
なお,Reactでは"npx"や"npm"がコマンドで利用されるが,以下の略称となる.
- npx: Node Package Execute
- npm: Node Package Management
$ npx create-react-app frontend
作成後,以下の構成となる.
以下コマンドで"frontend"ディレクトリに移動する.
$ cd frontend
以下コマンドを実行する.
$ npm start
ブラウザが開き,以下画面が出現する.
Terminalに戻り,以下コマンドを実行し,"ctrl + C"をクリックすると,以下メッセージが出力するので"y"ボタンをクリックし,Enterボタンをクリックする.
^C^Cバッチ ジョブを終了しますか (Y/N)? y
上記を実行すると,Terminalにてコマンドを打てるようになる."frontend"ディレクトリにいることを確認し,以下コマンドを実行する.ReactからHTTP requestsするためにAxiosをインストールする.
以下のようなメッセージが出力すれば問題ない.
$ npm install axios
# 上記コマンドにより出力するメッセージ
added 3 packages, and audited 1546 packages in 3s
262 packages are looking for funding
run `npm fund` for details
8 vulnerabilities (2 moderate, 6 high)
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
以下のようなディレクトリ構成となっている.
“src/App.js"を以下のように更新する.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import TodoForm from './TodoForm';
import TodoItem from './TodoItem';
function App() {
const [todos, setTodos] = useState([]);
useEffect(() => {
async function fetchTodos() {
const response = await axios.get('http://127.0.0.1:8000/api/todos/');
setTodos(response.data);
}
fetchTodos();
}, []);
const addTodo = (newTodo) => {
setTodos([...todos, newTodo]);
};
const toggleTodoCompletion = async (id, completed) => {
await axios.patch(`http://127.0.0.1:8000/api/todos/${id}/`, { completed });
setTodos(todos.map(todo => todo.id === id ? { ...todo, completed } : todo));
};
const incompleteTodos = todos.filter(todo => !todo.completed);
const completedTodos = todos.filter(todo => todo.completed);
return (
<div className="App">
<h1>Todo List</h1>
<TodoForm addTodo={addTodo} />
<h2>Existing Todos</h2>
<ul>
{incompleteTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} toggleTodoCompletion={toggleTodoCompletion} />
))}
</ul>
<h2>Completed Todos</h2>
<ul>
{completedTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} toggleTodoCompletion={toggleTodoCompletion} />
))}
</ul>
</div>
);
}
export default App;
“src"ディレクトリに"TodoItem.js"を作成し,"src/TodoItem.js"を以下のように更新する.
import React from 'react';
function TodoItem({ todo, toggleTodoCompletion }) {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodoCompletion(todo.id, !todo.completed)}
/>
<span style={{ display: 'inline-flex', gap: '10px' }}>
<strong>{todo.title}</strong>
{todo.attachment && (
<a href={todo.attachment} target="_blank" rel="noopener noreferrer">
View Attachment
</a>
)}
</span>
</li>
);
}
export default TodoItem;
“src"ディレクトリに"TodoForm.js"を作成し,"src/TodoForm.js"を以下のように更新する.
import React, { useState } from 'react';
import axios from 'axios';
function TodoForm({ addTodo }) {
const [title, setTitle] = useState('');
const [file, setFile] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData();
formData.append('title', title);
formData.append('completed', false);
if (file) formData.append('attachment', file);
const response = await axios.post('http://127.0.0.1:8000/api/todos/', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
addTodo(response.data);
setTitle(''); // Clear the title field after posting
setFile(null);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="New Todo"
required
/>
<input
type="file"
onChange={(e) => setFile(e.target.files[0])}
/>
<button type="submit">Add Todo</button>
</form>
);
}
export default TodoForm;
Terminalにを開き,"frontend"ディレクトリにて,以下コマンドを実行する.
$ npm start
ブラウザが開き,以下のようにTodoページが開けば完了となる.
Todoを追加し利用していくと,以下のようになる.
以上