TDLR :在django-rest-framework中实现标记的最佳方法是什么。标签的created_by
字段是当前经过身份验证的用户。
我正在努力实现一个非常简单/常见的事情,为帖子添加标签。但显然它不是小菜一碟。
所以我有一个帖子模型和一个标签模型(可能有很多关系)。我希望用户能够更新和创建帖子。在创建或更新帖子时,他应该能够更新帖子的标签。当帖子标记有新标记时,如果该标记存在,则应创建该标记。此外,我希望用户能够将标记指定为请求中的字符串列表。
示例请求
{
"name": "testpost1",
"caption": "test caption",
"tags": ["tag1", "tag2"],
},
models.py
class Tags(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
name = models.CharField(max_length=50, unique=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name="created_tags")
class Posts(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
name = models.CharField(max_length=50)
caption = models.TextField(max_length=1000)
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
tags = models.ManyToManyField('Tags', related_name='posts')
serializers.py
class TagsSerializerMini(serializers.ModelSerializer):
created_by = serializers.PrimaryKeyRelatedField(default=serializers.CurrentUserDefault(), queryset=User.objects.all())
class Meta:
model = Tags
fields = ('name', 'created_by')
extra_kwargs = {
'created_by': {'write_only': True},
'name': {'validators': []},
}
def create(self, validated_data):
tag, created = Tags.objects.get_or_create(**validated_data)
if not created:
raise exceptions.ValidationError(validated_data['name']+" already exists.")
return tag
def to_representation(self, instance):
ret = super(TagsSerializerMini, self).to_representation(instance)
data = dict()
data['name'] = ret['name']
return data
我尝试了两种方法。使用嵌套的序列化程序并使用与slug相关的字段。
使用SlugRealtedfield时,它会抛出标记对象dosent存在的验证错误。我正在计划如果我可以取消此检查,我可以在create()之前创建所有标签并调用super create。但我无法绕过验证检查。此外,我无法弄清楚如何将当前用户传递给slugrelatedfield。
经过一番搜索,我打算使用嵌套的序列化程序。但我必须将标签指定为dict [{"name":"tag1"}]
。此外,我必须定义自定义创建和更新。我可以让创建工作,但不是更新。
class PostsSerializer(QueryFieldsMixin, WritableNestedModelSerializer):
created_by = serializers.PrimaryKeyRelatedField(read_only=True, default=serializers.CurrentUserDefault())
class Meta:
model = Posts
fields = ('id', 'name', 'caption', 'tags', 'created_by')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['tags'] = TagsSerializerMini(many=True, required=False, context=self.context)
def create(self, validated_data):
tags_data = validated_data.pop('tags', [])
post = Posts.objects.create(**validated_data)
for tag in tags_data:
t, _ = Tags.objects.get_or_create(name=tag["name"])
post.tags.add(t)
return post
答案 0 :(得分:1)
在我看来,使用SlugRelatedField而不是嵌套的序列化器会更优雅,因为这样一来,您将拥有一个标签数组(和响应中的标签名称数组),而不是一个字典数组[{名称”:“标签名称”}}
正如您提到的,如果标签不存在,则验证检查将失败。 我设法通过子类化SlugRelatedField并重写“ to_internal_value”方法来克服此问题。在原始实现中,此方法尝试从查询集中获取对象,如果不存在对象,则验证失败。因此,不是调用“ get”方法,而是调用“ get_or_create”:
class CustomSlugRelatedField(serializers.SlugRelatedField):
def to_internal_value(self, data):
try:
obj, created = self.get_queryset().get_or_create(**{self.slug_field: data})
return obj
except (TypeError, ValueError):
self.fail('invalid')
答案 1 :(得分:0)
如果您可以接受使用两个字段,这是我的解决方案:
将 SlugRelatedField 用作只读,将 ListField 用作只读,因此您可以使用字符串列表而不是字典。
要获取当前用户,可以在序列化程序功能中使用self.context ['request']。user。
下面是示例代码(未经测试):
class PostsSerializer(serializers.ModelSerializer):
tags = serializers.SlugRelatedField(many=True, slug_field='name', read_only=True)
update_tags = serializers.ListField(
child=serializers.CharField(max_length=30), write_only=True)
class Meta:
model = Posts
exclude = ()
def create(self, validated_data):
tag_names = validated_data.pop('update_tags')
instance = super().create(validated_data)
user = self.context['request'].user
tags = []
for name in tag_names:
tag, created = Tags.objects.get_or_create(name=name, defaults={'created_by': user})
tags.append(tag)
instance.tags.set(tags)
return instance
def update(self, instance, validated_data):
tag_names = validated_data.pop('update_tags')
instance = super().update(instance, validated_data)
user = self.context['request'].user
tags = []
for name in tag_names:
tag, created = Tags.objects.get_or_create(name=name, defaults={'created_by': user})
tags.append(tag)
instance.tags.set(tags)
return instance
注意:我使用 instance.tags.set 而不是instance.tags.add,以便可以删除标记关系。您只需要始终发送所有标签。