Python | Django | React | Todo webアプリの作成方法

2024年10月3日

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を追加し利用していくと,以下のようになる.

以上