0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[React(Next.js)+Django Rest Framework] システムユーザの有効化・無効化用APIを自作してスライダーを作成しよう!

Last updated at Posted at 2023-12-28

概要

React/Next.jsを使って画面を作成し、Django Rest Frameworkを使ってAPIと疎通した上でスライダーのオン/オフの切り替えに応じて表示内容を変える方法について解説します
今回作成するスライダーのあるユーザ一覧画面は以下の通りです

スクリーンショット 2023-12-28 14.55.08.png

前提

  • Django、React(Next.js)のプロジェクトを作成済み
  • Material UIとTailwind CSSを使用しますが今回は説明しません
  • docker-compose.ymlとnginx.confを使用します。ソースコードは添付しますが詳細な説明は別記事のリンクを貼るのでそちらを参照してください

また、ログイン機能の続きとして記事を執筆しているのでまだ実装していない方は以下の記事を参考にしてください

ディレクトリ構成

tree
・
├── .gitignore
├── README.md
├── backend
│   ├── application
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── fixtures
│   │   │   └── fixture.json
│   │   ├── migrations
│   │   │   ├── __init__.py
│   │   │   └── 0001_initial.py
│   │   ├── models.py
│   │   ├── serializers.py
│   │   ├── urls.py
│   │   └── views.py
│   ├── manage.py
│   ├── poetry.lock
│   ├── project
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   └── pyproject.toml
├── containers
│   ├── django
│   │   ├── Dockerfile
│   │   ├── Dockerfile.prd
│   │   ├── entrypoint.prd.sh
│   │   └── entrypoint.sh
│   ├── nginx
│   │   ├── Dockerfile
│   │   └── nginx.conf
│   └── postgres
│       ├── Dockerfile
│       └── init.sql
├── docker-compose.yml
├── frontend
│   ├── README.md
│   ├── package-lock.json
│   ├── package.json
│   └── pages
│       └── users
│           └── index.tsx
└── static

docker-composeとNginxの設定

今回はフロントエンド側でAPIを使用するときは

http://localhost/back/

へリクエストを送るようdocker-composeとNginxの設定を行います
詳細は以下の記事の通りです

docker-compose.yml
version: '3.9'

services:
  db:
    container_name: db
    build:
      context: .
      dockerfile: containers/postgres/Dockerfile
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: pg_isready -U "${POSTGRES_USER:-postgres}" || exit 1
      interval: 10s
      timeout: 5s
      retries: 5
    environment:
      - POSTGRES_NAME
      - POSTGRES_USER
      - POSTGRES_PASSWORD
    ports:
      - '5432:5432' # デバッグ用

  app:
    container_name: app
    build:
      context: .
      dockerfile: containers/django/Dockerfile
    volumes:
      - ./backend:/code
      - ./static:/static
    ports:
      - '8000:8000'
      # デバッグ用ポート
      - '8080:8080'
    command: sh -c "/usr/local/bin/entrypoint.sh"
    stdin_open: true
    tty: true
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy

  nginx:
    container_name: web
    build:
      context: .
      dockerfile: containers/nginx/Dockerfile
    volumes:
      - ./static:/static
    ports:
      - 80:80
    depends_on:
      - app

volumes:
  db_data:
  static:

networks:
  default:
    name: testnet

nginx.conf
upstream front {
    server host.docker.internal:3000;
}

upstream back {
    server host.docker.internal:8000;
}
 
server {
    listen       80;
    server_name  localhost;

    client_max_body_size 5M;
    
    location / {
        proxy_pass http://front/;
    }
 
    location /back/ {
        proxy_set_header X-Forwarded-Host $host:$server_port;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://back/;
    }

    location /_next/webpack-hmr {
        proxy_pass http://front/_next/webpack-hmr;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

}

APIの実装

  • model
  • fixture
  • serilaizer
  • view

の順番に作成します

Modelの作成

models.py
import uuid

from django.contrib.auth.models import AbstractUser
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.core.validators import RegexValidator
from django.db import models


class User(AbstractUser):
    """システムユーザ"""

    username_validator = UnicodeUsernameValidator()

    class Role(models.IntegerChoices):
        """システムユーザのロール

        Args:
            MANAGEMENT(0): 管理者
            GENERAL(1):    一般
            PART_TIME(2):  アルバイト
        """

        MANAGEMENT = 0, "管理者"
        GENERAL = 1, "一般"
        PART_TIME = 2, "アルバイト"

    # 不要なフィールドはNoneにすることができる
    first_name = None
    last_name = None
    date_joined = None
    groups = None
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
        db_comment="システムユーザID",
    )
    employee_number = models.CharField(
        unique=True,
        validators=[RegexValidator(r"^[0-9]{8}$")],
        max_length=8,
        db_comment="社員番号",
    )
    username = models.CharField(
        max_length=150,
        unique=True,
        validators=[username_validator],
        db_comment="ユーザ名",
    )
    email = models.EmailField(
        max_length=254,
        unique=True,
        db_comment="メールアドレス",
    )
    role = models.PositiveIntegerField(
        choices=Role.choices,
        default=Role.PART_TIME,
        db_comment="システムユーザのロール",
    )
    created_at = models.DateTimeField(
        auto_now_add=True,
        db_comment="作成日",
    )
    updated_at = models.DateTimeField(
        auto_now=True,
        db_comment="更新日",
    )
    is_verified = models.BooleanField(
        default=False,
        db_comment="有効化有無",
    )

    USERNAME_FIELD = "employee_number"
    REQUIRED_FIELDS = ["email", "username"]

    class Meta:
        ordering = ["employee_number"]
        db_table = "User"
        db_table_comment = "システムユーザ"

    def __str__(self):
        return self.username

UserのModelについてはDjangoのAbstractUserを継承して作成します
詳細に知りたい方は以下の記事を参考にしてください

Fixtureの作成

システムユーザのテスト用データを作成します

fixture.json
[
    {
        "model": "application.User",
        "pk": 1,
        "fields": {
            "employee_number": "00000001",
            "username": "管理者ユーザユーザ01",
            "password": "pbkdf2_sha256$390000$KF4YHJxvWjSODaXdxLBg6S$U5XDh8mR77kMMUtlRcBZS/bkaxdpjNR/P4zyy25g3/I=",
            "email": "test01@example.com",
            "role": 0,
            "is_superuser": 0,
            "is_verified": true,
            "created_at": "2022-07-28T00:31:09.732Z",
            "updated_at": "2022-07-28T00:31:09.732Z",
            "created_by": 1,
            "updated_by": 1
        }
    },
    {
        "model": "application.User",
        "pk": 2,
        "fields": {
            "employee_number": "00000002",
            "username": "一般ユーザ01",
            "password": "pbkdf2_sha256$390000$KF4YHJxvWjSODaXdxLBg6S$U5XDh8mR77kMMUtlRcBZS/bkaxdpjNR/P4zyy25g3/I=",
            "email": "test02@example.com",
            "role": 1,
            "is_superuser": 0,
            "is_verified": true,
            "created_at": "2022-07-28T00:31:09.732Z",
            "updated_at": "2022-07-28T00:31:09.732Z",
            "created_by": 1,
            "updated_by": 1
        }
    },
    {
        "model": "application.User",
        "pk": 3,
        "fields": {
            "employee_number": "00000003",
            "username": "スーパーユーザ01",
            "password": "pbkdf2_sha256$390000$KF4YHJxvWjSODaXdxLBg6S$U5XDh8mR77kMMUtlRcBZS/bkaxdpjNR/P4zyy25g3/I=",
            "email": "test04@example.com",
            "role": 0,
            "is_superuser": 1,
            "is_verified": true,
            "created_at": "2022-07-28T00:31:09.732Z",
            "updated_at": "2022-07-28T00:31:09.732Z",
            "created_by": 1,
            "updated_by": 1
        }
    }
]

Serilaizerの作成

システムユーザの一覧表示用のserializerを使用します

serializers.py
from rest_framework import serializers

from application.models import User


class UserSerializer(serializers.ModelSerializer):
    """ユーザ用シリアライザ"""

    class Meta:
        model = User
        fields = [
            "id",
            "employee_number",
            "username",
            "email",
            "role",
            "is_active",
            "is_verified",
        ]
        read_only_fields = [
            "id",
            "created_at",
            "updated_at",
            "is_active",
            "is_verified",
        ]

    def to_representation(self, instance):
        rep = super(UserSerializer, self).to_representation(instance)
        rep["role"] = instance.get_role_display()
        return rep

Viewの作成

  • システムユーザ一覧表示用API
  • システムユーザの有効化・無効化用API

の2種類を作成します

views.py
from django.http import JsonResponse
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet

from application.models import User
from application.serializers import UserSerializer


class UserViewSet(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [IsAuthenticated]

    
    @action(detail=True, methods=["post"])
    def toggle_user_active(self, request, pk):
        """ユーザを有効化/無効化するAPI
        Args:
            request : リクエスト
            pk : ユーザID
        Returns:
            JsonResponse
        """
        user = self.get_object()
        if request.user == user:
            return JsonResponse(
                data={"msg": "自身を無効化することはできません"},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if user.is_active:
            user.is_active = False
        else:
            user.is_active = True
        user.save()
        return JsonResponse(data={"is_active": user.is_active})

ユーザを有効化/無効化するAPIを作成します
その際にユーザが自信を無効化しようとした場合、

{
    "msg": "自身を無効化することはできません"
}

というエラーメッセージをJSONで返します
システムユーザを有効・無効化するAPIの詳細は以下の記事を参考にしてください

お客様一覧画面

お客様一覧画面のコードは以下の通りです

index.tsx
import { useState, useEffect } from "react";
import Cookies from "js-cookie";
import router from "next/router";
import Table from "@mui/material/Table";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import { BasicMenu } from "@/components/buttons/MenuButton";
import { Switch, Button } from "@mui/material";
import SendIcon from "@mui/icons-material/Send";

type UserData = {
  id: number;
  employee_number: string;
  username: string;
  email: string;
  role: string;
  is_active: boolean;
  is_verified: boolean;
};

type UserArray = UserData[];

function UserList() {
  const [data, setData] = useState<UserArray>([]);
  const [loggedIn, setLoggedIn] = useState<Boolean>(true); // ログイン状態を管理

  const fetchActive = async (id: string) => {
    try {
      // fetchAPIの処理を記載する
      const apiUrl = `${process.env["NEXT_PUBLIC_API_URL"]}/api/users/${id}/toggle_user_active`;
      const csrftoken = Cookies.get("csrftoken") || "";
      const credentials = "include";

      const response = await fetch(apiUrl, {
        method: "POST",
        headers: {
          "Content-type": "application/json",
          "X-CSRFToken": csrftoken,
          "Cache-Control": "private",
        },
        credentials: credentials,
      });
      if (response.ok) {
        setLoggedIn(true);
        fetchUserData();
      } else if (response.status === 403) {
        setLoggedIn(false);
        router.push("/"); // ログインしていない場合にルートページにリダイレクト
      } else if (response.status === 404) {
        router.replace("/404"); // IDが存在しない場合は404ページへリダイレクト
      } else if (response.status === 400) {
        response.json().then((data) => {
          const msg = data.msg;
          alert(msg);
        });
      } else {
        alert("エラーが発生しました");
      }
    } catch (error) {
      if (error instanceof Error) {
        alert({ message: `${error.message}`, severity: "error" });
      }
    }
  };

  const switchHandler = (switchData: { id: String }) => {
    fetchActive(switchData.id);
  };

  const fetchUserData = async () => {
    try {
      // http://localhost/back/がNEXT_PUBLIC_API_URLに入る
      const apiUrl = `${process.env["NEXT_PUBLIC_API_URL"]}/api/users`;
      const csrftoken = Cookies.get("csrftoken") || "";
      const credentials = "include";

      const response = await fetch(apiUrl, {
        method: "GET",
        headers: {
          "X-CSRFToken": csrftoken,
        },
        credentials: credentials,
      });

      if (response.ok) {
        const responseData: UserArray = await response.json();
        setData(responseData);
        setLoggedIn(true);
      } else if (response.status === 403) {
        setLoggedIn(false);
      } else {
        alert("エラーが発生しました");
      }
    } catch (error) {
      console.error("データの取得に失敗しました:", error);
    }
  };

  useEffect(() => {
    fetchUserData();
  }, []);

  useEffect(() => {
    if (!loggedIn) {
      router.push("/");
    }
  }, [loggedIn]);

  if (!data || !data.results) return null;

  return (
    <div className="customer-list">
      <BasicMenu />
      <br />
      <div>
        <h1 className="flex flex-col items-center my-[10px] text-3xl text-gray-900">
          システムユーザ一覧
        </h1>
        <div className="flex flex-col items-end my-[10px]">
          <Button
            type="submit"
            size="medium"
            variant="contained"
            color="primary"
            className="grid justify-items-end w-[200px] my-[20px]"
          >
            ユーザ招待
          </Button>
        </div>
        <Table>
          <TableHead>
            <TableRow>
              <TableCell align="center" className="font-bold">
                社員番号
              </TableCell>
              <TableCell align="center" className="font-bold">
                システムユーザ名
              </TableCell>
              <TableCell align="center" className="font-bold">
                メールアドレス
              </TableCell>
              <TableCell align="center" className="font-bold">
                権限
              </TableCell>
              <TableCell align="center" className="font-bold">
                有効/無効
              </TableCell>
              <TableCell align="center" className="font-bold">
                再送信
              </TableCell>
              <TableCell align="center" className="font-bold"></TableCell>
            </TableRow>
          </TableHead>
          {data.results.map((item, index) => (
            <TableBody key={index}>
              <TableCell align="center">{item.employee_number}</TableCell>
              <TableCell align="center">{item.username}</TableCell>
              <TableCell align="center">{item.email}</TableCell>
              <TableCell align="center">{item.role}</TableCell>
              <TableCell align="center">
                <Switch
                  checked={item.is_active}
                  onChange={() => switchHandler({ id: item.id })}
                />
              </TableCell>
              <TableCell align="center">
                <Button
                  disabled={item.is_verified}
                  size="small"
                  variant="contained"
                  color="success"
                  className="w-[100px] my-[10px]"
                >
                  再送信
                  <SendIcon />
                </Button>
              </TableCell>
              <TableCell align="center">
                <Button
                  size="small"
                  variant="contained"
                  className="w-[100px] my-[10px]"
                >
                  編集
                </Button>
              </TableCell>
            </TableBody>
          ))}
        </Table>
      </div>
    </div>
  );
}

export default UserList;

一つずつ解説していきます

一覧表示のAPIからデータを取得

fetchUserDataという関数を使って関数内にAPIを実行し、response.okの場合はsetDataメソッドを実行し、dataのArrayに入れる処理を記載します
ログインしているか知りたいのでuseStateを使ってresponse.okだったらloggedInをTrueに、ステータスコードが403の場合はloggedInをFalseにします
useEffectを使って初回レンダリング時のみデータを取得する処理を実行するようにします

  const fetchUserData = async () => {
    try {
      // http://localhost/back/がNEXT_PUBLIC_API_URLに入る
      const apiUrl = `${process.env["NEXT_PUBLIC_API_URL"]}/api/users`;
      const csrftoken = Cookies.get("csrftoken") || "";
      const credentials = "include";

      const response = await fetch(apiUrl, {
        method: "GET",
        headers: {
          "X-CSRFToken": csrftoken,
        },
        credentials: credentials,
      });

      if (response.ok) {
        const responseData: UserArray = await response.json();
        setData(responseData);
        setLoggedIn(true);
      } else if (response.status === 403) {
        setLoggedIn(false);
      } else {
        alert("エラーが発生しました");
      }
    } catch (error) {
      console.error("データの取得に失敗しました:", error);
    }
  };

ログイン有無の確認

useEffectを使ってloggedInの変数が変わったタイミングで処理を実施します
loggedInがFalseの場合はルートページへリダイレクトさせます
ログイン機能について詳細に知りたい方は以下の記事を参考にしてください

  useEffect(() => {
    if (!loggedIn) {
      router.push("/");
    }
  }, [loggedIn]);

スライダーの実装

checked内にAPIから取得したis_activeが入リます
ボタンが押された時はswitchHandler関数が起動し、引数としてuserのidが入ります

<Switch
  checked={item.is_active}
  onChange={() => switchHandler({ id: item.id })}
/>

switchHandlerの実装

今回はスライダーを押したら確認画面を表示させるなど追加の処理を作成する予定がないのでswitchHandlerを実行するとfetchActiveが実行されるようにします

  const switchHandler = (switchData: { id: String }) => {
    fetchActive(switchData.id);
  };

一覧表示同様、APIを実行し、200の時はもう一度fetchUserData()を実行し、一覧画面を更新します
また、400の時はresponse内のエラーメッセージをalertでそのまま表示させます

  const fetchActive = async (id: string) => {
    try {
      // fetchAPIの処理を記載する
      const apiUrl = `${process.env["NEXT_PUBLIC_API_URL"]}/api/users/${id}/toggle_user_active`;
      const csrftoken = Cookies.get("csrftoken") || "";
      const credentials = "include";

      const response = await fetch(apiUrl, {
        method: "POST",
        headers: {
          "Content-type": "application/json",
          "X-CSRFToken": csrftoken,
          "Cache-Control": "private",
        },
        credentials: credentials,
      });
      if (response.ok) {
        setLoggedIn(true);
        fetchUserData();
      } else if (response.status === 403) {
        setLoggedIn(false);
        router.push("/"); // ログインしていない場合にルートページにリダイレクト
      } else if (response.status === 404) {
        router.replace("/404"); // IDが存在しない場合は404ページへリダイレクト
      } else if (response.status === 400) {
        response.json().then((data) => {
          const msg = data.msg;
          alert(msg);
        });
      } else {
        alert("エラーが発生しました");
      }
    } catch (error) {
      if (error instanceof Error) {
        alert({ message: `${error.message}`, severity: "error" });
      }
    }
  };

実際に触ってみよう

まず、以下のように一覧で表示できたら成功です

スクリーンショット 2023-12-28 14.55.08.png

スライダーが切り替わったら成功です

スクリーンショット 2023-12-28 15.23.48.png

自身を無効化しようとしてアラートが表示されたら成功です

スクリーンショット 2023-12-28 15.24.37.png

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?