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

## 実行環境
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681173/2568a68d-8947-ff1a-721c-effb070657de.png)
更新結果画面
![スクリーンショット 2021-07-01 16.59.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681173/d15ec54c-bc38-2f5e-1b1d-e0da59e324a3.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681173/c6b90125-6613-4953-5c4b-f39b3652c144.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/