LoginSignup
4
3

More than 1 year has passed since last update.

[Django REST Framework][React]タグ機能作成

Last updated at Posted at 2021-07-01

実行環境

MacOS BigSur -- 11.4
Python3 -- 3.8.2
Django -- 3.1.7
djangorestframework -- 3.12.2
npm -- 6.14.4
react -- 17.0.2
axios -- 0.21.1
react-tag-input -- 6.7.1

DRFとReactでタグ機能を実装

Django REST FrameworkとReactで開発中のアプケーションに、タグ機能実装したので今回はその手順を記します。React側のタグ作成はいくつかの便利なライブラリがあるようで、その中でも今回は「react-tag-input」を使用しました。UserProfileModelにプロフィールとタグのフィールドを用意し、ログイン中ユーザーのプロフィールの更新を行います。タグフィールドはTagModelその際にタグがない場合は、TagModelに新規登録した上で更新を行います。ここが結構ややこしかったです。。

完成イメージ

更新画面
スクリーンショット 2021-07-01 16.59.00.png
更新結果画面
スクリーンショット 2021-07-01 16.59.08.png

DRF側の基本実装

Model

models.py
# 一部抜粋
class Tag(models.Model):
    name = models.CharField('タグ', primary_key=True, unique=True, max_length=20)

    def __str__(self):
        return self.name

class UserProfile(AbstractBaseUser, PermissionsMixin):
    name = models.CharField(primary_key=True, max_length=255)
    profile = models.TextField(max_length=500, blank=True)
    tag = models.ManyToManyField(Tag, verbose_name='タグ', blank=True)
    REQUIRED_FIELDS = ['name']

    def __str__(self):
        return self.name

UserProfileModelでtagをManyToManyFieldで紐づけています。

Serializer

serializers.py
# 一部抜粋
class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ('name',)

class UserProfileSerializer(serializers.ModelSerializer):
    class Meta:
        # Serializersに紐付けるmodelを定義
        model = models.UserProfile
        # 管理したい項目を定義(タプル形式)
        fields = ('id', 'email', 'name', 'password', 'profile', 'tag', 'image')
        extra_kwargs = {
            'password':{
                # セキュリティの関係上パスワードは書き込むだけ。
                'write_only': True,
                # パスワード入力の際に「・・・」となるようにstyleを指定
                'style': {'input_type': 'password'}
            }
        }

    # ModelSerializerにデフォルトでupdate()が実装されているが、passwordハッシュ化のためオーバーライド
    def update(self,instance,validated_data):
        """Handle updating user account"""
        if 'password' in validated_data:
            password=validated_data.pop('password')
            instance.set_password(password)
            print('hello')

        return super().update(instance,validated_data)

View

views.py
# 一部抜粋
class TagViewSet(viewsets.ModelViewSet):
    serializer_class = TagSerializer
    queryset = Tag.objects.all()

class UserProfileViewSet(viewsets.ModelViewSet):

    serializer_class=serializers.UserProfileSerializer
    queryset=models.UserProfile.objects.all()

    @action(detail=True, methods=['patch'])
    def patch_profile(self, request, pk=None):
        for i in request.data['tag']:
            # 入力されたタグがモデルにない場合は新規作成
            if not Tag.objects.filter(name=i).exists():
                tag_serializer = TagSerializer(data={'name':i})
                if tag_serializer.is_valid():
                    tag_serializer.save()
        data={'email':request.data['email'],'profile':request.data['profile'],'tag':request.data['tag']}
        serializer = self.serializer_class(request.user, data=data, partial=True)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)

Viewが最も苦戦した部分で、アクションデコレーターを用いてプロフィール更新のための具体的処理を書いています。
@action(detail=True, methods=['patch'])
今回はReact側から更新のためPATCHメソッドを用いているので、methods=['patch']としています。
このアクションデコレーターにより作成したインスタンスメソッドで、URLにアクセスした際に任意の処理を追加できます。

ハマったポイントとしては、以下にあるserializerのupdate()にどう引数を渡すかという部分です。

serializer = self.serializer_class(request.user, data=data, partial=True)
      if serializer.is_valid():
             serializer.save()

そもそもserializerのsave()メソッドは、第一引数が無い場合にはcreate()メソッド、第一引数がある場合はupdate()メソッドがコールされるようです。これを知らずにずっと第一引数を記述しておらず、八方塞がりでした。。ですので、serializerの第一引数に更新対象のインスタンスを指定することで、無事にupdate()が呼び出されます。
partial=Trueは、部分更新を行うため記載しています。

URL

そして今回はModelViewSetを用いているのでURLは簡単にこんな感じです。

urls.py
# 一部抜粋
router = DefaultRouter()
router.register(r'profile',views.UserProfileViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

React側の基本実装

react-tag-inputのインストール

$ npm install react-tag-input
$ npm install --save @pathofdev/react-tag-input

プロフィール更新用コンポーネント

実際は認証処理や、Reduxを用いたユーザー情報のストアへの格納処理をしていますが、今回はプロフィールの更新用のコンポーネントの該当部分だけ抜粋して記載します。ちなみにUIは主にreact-bootstrapを使用しています。

UpdateProfile.js
import React, { useState } from 'react';
import axios from 'axios';
import  { useHistory } from 'react-router-dom';
import { apiURL } from './Default';
import Cookies from 'universal-cookie';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
// 以下、今回追加部分
import ReactTagInput from "@pathofdev/react-tag-input";
import "@pathofdev/react-tag-input/build/index.css";

const cookies = new Cookies();

const UpdateProfile = () => {

    const history = useHistory();

    const { register, handleSubmit, formState: { errors } } = useForm();
    const username = useSelector(state => state.user.name);
    const email = useSelector(state => state.user.email);
    const profile = useSelector(state => state.user.profile);
    const mytags = useSelector(state => state.user.tags);
    const [emailnew, setEmailnew] = useState(email);
    const [profilenew, setProfilenew] = useState(profile);
    const [tags, setTags] = React.useState(mytags);

    const changeEmail = (e) => {
      setEmailnew(() => e.target.value);
    }
    const changeProfile = (e) => {
      setProfilenew(() => e.target.value);
    }
    const changeTags = (tags) => {
      setTags(tags);
    }

// 更新ボタンが押されたら
    const update = async (data) =>{
        await axios.patch(`${apiURL}profile/`+username+'/patch_profile/',{
            email: emailnew,
            profile: profilenew,
            tag: tags,
          },{
            headers: {
              'Content-Type': 'application/json',
              'Authorization': `JWT ${cookies.get('accesstoken')}`
            }
          }).then(res =>{
          alert("更新完了!");
          history.push('/mypage');
          }).catch(err => {
            alert("エラー:"+err);
          })
        };

    return (
        <div className="top-wrapper">
          <h3>プロフィール編集</h3>
          <div className="profile_update_field">
          <Form>
            <Form.Group as={Row} controlId="formPlaintextUsername">
              <Form.Label column sm="2">
                ユーザー名
              </Form.Label>
              <Col sm="10">
              <Form.Control size="lg" plaintext readOnly defaultValue={username}/>
              </Col>
            </Form.Group>

            <Form.Group as={Row} controlId="formPlaintextEmail">
              <Form.Label column sm="2">
                メールアドレス
              </Form.Label>
              <Col sm="10">
              <Form.Control size="lg" type="text" defaultValue={email} onChange={changeEmail}/>
              </Col>
            </Form.Group>

            <Form.Group as={Row} controlId="formPlaintextProfile">
              <Form.Label column sm="2">
                プロフィール
              </Form.Label>
              <Col sm="10">
              <Form.Control size="lg" as="textarea" defaultValue={profile} onChange={changeProfile}/>
              </Col>
            </Form.Group>

            <Form.Group as={Row} controlId="formTag">
              <Form.Label column sm="2">
                タグ
              </Form.Label>
              <Col sm="10">
              <ReactTagInput
                placeholder="入力してください"
                tags={tags}
                onChange={changeTags}
              />
              </Col>
            </Form.Group>

          </Form>
          </div>
          <Button variant="success" onClick={update}>更新</Button>
        </div>
    );
  }

  export default UpdateProfile;

まず、インストールしたreact-tag-inputライブラリをimportします。その際に"@pathofdev/react-tag-input/build/index.css"も同時にimportしないとエラーが出ます。
そして、今回はreact-bootstrapのForm内に配置していますが、importしたReactTagInputコンポーネントを埋め込みます。placeholderでプレースホルダ文字列や、tagsで初期値などを指定できます。今回は、タグの初期値はstoreに格納しているユーザーが設定している更新前のタグを設定しています。これにより、以下のようなタグ入力用フォームが出来上がります。結構見た目もシンプルで、気に入っています^^
スクリーンショット 2021-07-01 17.41.20.png

axios.patch(`${apiURL}profile/`+username+'/patch_profile/',  ...

最後に、この部分でDRF側のpatch_profileにPATCHリクエストを送り、DRFで実装したタグ新規登録やプロフィールアップデートの処理が行われています。

今回は以上でタグ機能の実装を完了しました。ここからタグのバリデーションや検索機能を追加していこうと思います。

参考

以下のページが非常に分かりやすく、参考にさせていただきました。
- https://note.crohaco.net/2018/django-rest-framework-serializer/
- http://note.crohaco.net/2018/django-rest-framework-view/

4
3
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
4
3