Django休息框架获取或创建PrimaryKeyRelatedField

时间:2017-05-14 12:41:06

标签: django rest tags django-rest-framework relationship

我开始使用Django和Django休息框架为我的Web应用程序创建REST API,我需要一个逻辑问题。

有实体指令和标签。用户访问我的服务并创建自我指令并为其添加存在标记或新标记。

我使用PrimaryKeyRelatedField创建了我的模型seriallizer类,用于关系指令< - > Tag。但是,如果我使用新标签对新指令进行POST,则会收到错误:“无效的pk \”标记名\“ - 对象不存在。”。 我在字段类中覆盖 to_internal_value 方法解决了这个问题。

解决此问题的最佳做法是什么?在我看来,这个问题对于Web和REST API来说是典型的。

我的模特:

class Tag(Model):
    name = CharField(max_length=32, verbose_name=_("Name"),
                     unique=True, validators=[alphanumeric], primary_key=True)

    def __str__(self):
        return self.name


class Instruction(Model):
    user = ForeignKey(settings.AUTH_USER_MODEL,
                      related_name='instructions',
                      on_delete=CASCADE,
                      blank=False, null=False,
                      verbose_name=_("User"))
    title = CharField(max_length=256,
                      verbose_name=_("Title"),
                      blank=False, null=False)
    created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
    modified_datetime = DateTimeField(
        verbose_name=_("Last modification time"), blank=False, null=False)
    tags = ManyToManyField(Tag,
                           related_name="instructions",
                           verbose_name=_("Tags"))

    class Meta:
        ordering = ['-created_datetime']
        # singular_name = _("")

    def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
        n = now()
        if self.id is None:
            self.created_datetime = n
        self.modified_datetime = n
        super(Instruction, self).save(force_insert, force_update, using, update_fields)

    def __str__(self):
        return self.title

我的序列化器:

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ('name',)

class InstructionSerializer(serializers.ModelSerializer):
    tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
    author = serializers.SerializerMethodField()

    def get_author(self, obj):
        return obj.user.username

    class Meta:
        model = Instruction
        fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author')
        read_only_fields = ('modified_datetime',)

我创建了新的字段类class PrimaryKeyCreateRelatedField 并覆盖了 to_internal_value 方法,用于创建新的Tag对象而不是使用消息'does_not_exist'进行提升:

PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):

    def to_internal_value(self, data):
        if self.pk_field is not None:
            data = self.pk_field.to_internal_value(data)
        try:
            return self.get_queryset().get(pk=data)
        except ObjectDoesNotExist:
            # self.fail('does_not_exist', pk_value=data)
            return self.get_queryset().create(pk=data)
        except (TypeError, ValueError):
            self.fail('incorrect_type', data_type=type(data).__name__)

我的观点:

class InstructionViewSet(viewsets.ModelViewSet):
    queryset = Instruction.objects.all()
    serializer_class = InstructionSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

    def create(self, request, *args, **kwargs):
        data = dict.copy(request.data)
        data['user'] = self.request.user.pk

        serializer = InstructionSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

更新

models.py

alphanumeric = RegexValidator(r'^[0-9a-zA-Z]*$',
                              _('Only alphanumeric characters are allowed.'))


class Tag(Model):
    name = CharField(max_length=32, verbose_name=_("Name"),
                     unique=True, validators=[alphanumeric], primary_key=True)

    def __str__(self):
        return self.name


class Step(PolymorphicModel):
    instruction = ForeignKey(Instruction,
                             verbose_name=_("Instruction"),
                             related_name='steps',
                             blank=False, null=False,
                             on_delete=CASCADE)
    position = PositiveSmallIntegerField(verbose_name=_("Position"), default=0)

    description = TextField(verbose_name=_("Description"),
                            max_length=2048,
                            blank=False, null=False)

    class Meta:
        verbose_name = _("Step")
        verbose_name_plural = _("Steps")
        ordering = ('position',)
        unique_together = ("instruction", "position")

    def __str__(self):
        return self.description[:100]


class Instruction(Model):
    user = ForeignKey(settings.AUTH_USER_MODEL,
                      related_name='instructions',
                      on_delete=CASCADE,
                      blank=False, null=False,
                      verbose_name=_("User"))
    title = CharField(max_length=256,
                      verbose_name=_("Title"),
                      blank=False, null=False)
    created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
    modified_datetime = DateTimeField(
        verbose_name=_("Last modification time"), blank=False, null=False)
    tags = ManyToManyField(Tag,
                           related_name="instructions",
                           verbose_name=_("Tags"))

    # thumbnail = #TODO: image field

    class Meta:
        ordering = ['-created_datetime']
        # singular_name = _("")

    def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
        n = now()
        if self.id is None:
            self.created_datetime = n
        self.modified_datetime = n
        super(Instruction, self).save(force_insert, force_update, using, update_fields)

    def __str__(self):
        return self.title

views.py

class InstructionViewSet(viewsets.ModelViewSet):
    queryset = Instruction.objects.all()
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

    def get_serializer_class(self):
        """Return different serializer class for different action."""
        if self.action == 'list':
            return InstructionSerializer
        elif self.action == 'create':
            return InstructionCreateSerializer

serialiers.py

class PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):

    def to_internal_value(self, data):
        if self.pk_field is not None:
            data = self.pk_field.to_internal_value(data)
        try:
            return self.get_queryset().get(pk=data)
        except ObjectDoesNotExist:
            # self.fail('does_not_exist', pk_value=data)
            return self.get_queryset().create(pk=data)
        except (TypeError, ValueError):
            self.fail('incorrect_type', data_type=type(data).__name__)


class InstructionCreateSerializer(serializers.ModelSerializer):
    tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
    steps = InstructionStepSerializer(many=True)
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())

    class Meta:
        model = Instruction
        fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'steps')
        read_only_fields = ('modified_datetime',)

    def create(self, validated_data):
        tags_data = validated_data.pop('tags')
        steps_data = validated_data.pop('steps')

        # NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
    # "needs to have a value for field "id" before this many-to-many relationship can be used."
        instruction = Instruction.objects.create(**validated_data)

        for tag in tags_data:
            instruction.tags.add(tag)

        for step in steps_data:
            Step.objects.create(instruction=instruction,
                                description=step['description'],
                                position=step['position'])
        return instruction


class InstructionSerializer(serializers.ModelSerializer):
    tags = serializers.StringRelatedField(many=True)
    author = serializers.SerializerMethodField()
    steps = InstructionStepSerializer(many=True)

    def get_author(self, obj):
        return obj.user.username

    class Meta:
        model = Instruction
        fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author', 'steps')
        read_only_fields = ('modified_datetime',)

4 个答案:

答案 0 :(得分:1)

在“常规”Django中,您通常希望以表单的save方法创建模型实例,而不是视图。 DRF类似,因为您希望在序列化程序的createupdate方法中创建模型实例。原因是如果您需要向API添加新端点,则可以重用序列化程序,而不必编写重复代码来创建或更新模型实例。

以下是我重构代码的方法:

  • create中移除整个ModelViewSet方法 - 您无需覆盖该方法。
  • 删除自定义PrimaryKeyCreateRelatedField - 您只需要PrimaryKeyRelatedField
  • 向序列化程序添加两种方法 - createupdate
    • create方法中,在保存tag对象之前创建instruction个对象,就像您可以看到in the DRF docs一样。您可以通过此self.context['request'].user方法中的create,在视图中获取当前用户。因此,您可以创建Instruction Instruction.objects.create(user=self.context['request'].user, **validated_data),然后循环浏览tags(就像文档中的tracks一样),将它们添加到Instruction
    • 文档没有示例update方法,但基本上您的update方法也会为现有instance采用instruction参数。有关详细信息,请参阅this answer from the creator of DRF

答案 1 :(得分:1)

除了@YPCrumble和@SijanBhandari给出的答案之外,我只需要对你的代码中的某些内容发表评论。

在models.py中,您已重写了添加created_at和modified_on的save方法。为此你可以添加

created_at = models.DateTimeField(auto_now_add=True)
modified_on = DateTimeField (auto_now=True)

auto_now_add选项设置第一次创建对象的时间。 它不可编辑。无论何时保存对象,即每当调用object.save()方法时,auto_now设置都会置位。

这些通常用于为对象加时间戳以供将来参考。

为什么要编写这么多行,只需2行代码即可。 只是抬起头来!!

有关详细信息,请go to the documentation here

答案 2 :(得分:1)

在我解决问题的情况下,我需要覆盖方法run_validation。这允许在验证之前检查tags并创建它们(如果不存在)。

class InstructionCreateSerializer(serializers.ModelSerializer):
    steps = InstructionStepSerializer(many=True)
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())

    class Meta:
        model = Instruction
        fields = ('title', 'created_datetime', 'modified_datetime', 'tags', 'steps', 'id', 'user')
        read_only_fields = ('modified_datetime',)

    def run_validation(self, data=serializers.empty):
        if 'tags' in data:
            for tag in data['tags']:
                Tag.objects.get_or_create(name=tag)
        return super(InstructionCreateSerializer, self).run_validation(data)

    def create(self, validated_data):
        tags_data = validated_data.pop('tags')
        steps_data = validated_data.pop('steps')

        # NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
        # "needs to have a value for field "id" before this many-to-many relationship can be used."
        instruction = Instruction.objects.create(**validated_data)

        for tag in tags_data:
            instruction.tags.add(tag)

        for step in steps_data:
            Step.objects.create(instruction=instruction,
                                description=step['description'],
                                position=step['position'])
        return instruction

答案 3 :(得分:0)

最好的方法是在视图的CREATE方法中对所有内容进行排序。

我相信你的标签会以

的格式从你的前端发送到后端
[   1,
    {'name': "TEST"},
    {'name': 'TEST2'}
]

此处'1'是现有标记ID,'TEST''TEST2'是插入的两个新标记 用户。现在,您可以按如下方式更改CREATE方法:

class InstructionViewSet(viewsets.ModelViewSet):
    queryset = Instruction.objects.all()
    serializer_class = InstructionSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

    def create(self, request, *args, **kwargs):
        data = dict.copy(request.data)
        data['user'] = self.request.user.pk

        # MODIFICATION.....
        tags = self.request.get('tags', None)
        tag_list = []
        if tags:
            for tag in tags:
                if isinstance(tag, dict):
                    new_tag = Tag.objects.create(name=tag['name'])
                    tag_list.append(new_tag.id)
                else:
                    tag_list.append(int(tag))


        data = {

        'title': ....
        'tags': tag_list,
        'user': ...
        'author': ...
        ......



        }

        serializer = InstructionSerializer(data=data)

我希望它会对你有所帮助。