Django Rest Framework set a field read_only after

2019-07-19 16:01发布

问题:

I'm using Django 2.x and Django REST Framework.

I have a model with contact as a foreign key

class AmountGiven(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    contact = models.ForeignKey(Contact, on_delete=models.PROTECT)
    amount = models.FloatField(help_text='Amount given to the contact')
    given_date = models.DateField(default=timezone.now)
    created = models.DateTimeField(auto_now=True)

and serializer like

class AmountGivenSerializer(serializers.ModelSerializer):
    mode_of_payment = serializers.PrimaryKeyRelatedField(queryset=ModeOfPayment.objects.all())
    contact_detail = ContactSerializer(source='contact', read_only=True)
    contact = serializers.PrimaryKeyRelatedField(queryset=Contact.objects.all())

    class Meta:
        model = AmountGiven
        depth = 1
        fields = (
            'id', 'contact', 'contact_detail', 'amount', 'given_date', 'created'
        )

contact field is required while creating a new record. But I do not want contact to be modified once it is created.

But when I send only amount with PUT method it says

{
    "contact": [
        "This field is required."
    ]
}

And when I use PATCH method, it works fine but if passing some other value for contact, it is updating contact as well.

I want to make contact field not-required while updating existing record. And even if it is passed, use the earlier one instead of setting the new data.

Trial 2

I tried overriding the contact field in the request to the previously stored value so that in case if changed contact is passed or no contact is passed, it will save earlier one.

So, in the viewset add the function

def update(self, request, *args, **kwargs):
    obj = self.get_object()
    request.data['contact'] = obj.contact_id
    return super().update(request, *args, **kwargs)

But this is giving error as

This QueryDict instance is immutable

回答1:

Use __init__ method of serializer to make it read when object is being updated:

class AmountGivenSerializer(serializers.ModelSerializer):  

    def __init__(self, *args, **kwargs):
        """If object is being updated don't allow contact to be changed."""
        super().__init__(*args, **kwargs)
        if self.instance is not None:
            self.fields.get('parent').read_only = True
            # self.fields.pop('parent') # or remove the field


    mode_of_payment = serializers.PrimaryKeyRelatedField(queryset=ModeOfPayment.objects.all())
    contact_detail = ContactSerializer(source='contact', read_only=True)
    contact = serializers.PrimaryKeyRelatedField(queryset=Contact.objects.all())

    class Meta:
        model = AmountGiven
        depth = 1
        fields = (
            'id', 'contact', 'contact_detail', 'amount', 'given_date', 'created'
        )

Using self.context['view'].action is not recommended as it will not work when using the serializer out of DRF, eg. in normal Django views. It's best to use self.instance as it will work in every situation.



回答2:

If your viewset is a ModelViewSet, you can overwrite the perform_update hook (because ModelViewSet inherits from GenericAPIView (take a look at "Save and deletion hooks"). You can access the old contact using the serializer's instance field:

class MyViewSet(viewsets.ModelViewSet):
    # ... other stuff

    def perform_update(self, serializer):
        serializer.save(contact=serializer.instance.contact)

So you will have to provide a contact, but no matter which contact you provide, it will always use the old saved contact when updating.