実行環境
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に新規登録した上で更新を行います。ここが結構ややこしかったです。。
完成イメージ
DRF側の基本実装
Model
# 一部抜粋
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
# 一部抜粋
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
# 一部抜粋
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は簡単にこんな感じです。
# 一部抜粋
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を使用しています。
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に格納しているユーザーが設定している更新前のタグを設定しています。これにより、以下のようなタグ入力用フォームが出来上がります。結構見た目もシンプルで、気に入っています^^
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/