Django rest framework not creating object with FK

2019-09-14 16:29发布

问题:

I have two models like this:

class Sector(models.Model):
    name = models.CharField(max_length=100, db_index=True, unique=True)  # HERE IF I REMOVE unique=True, it works correctly

class Address(models.Model):
    ...
    sector = models.ForeignKey(Sector, null=True, blank=True)

And a serializer for the Address model:

In the view, I have this:

    address_serialized = AddressSerializer(data=request.data)
    if address_serialized.is_valid():
        address_serialized.save(client=client)

It never gets to the create function. I have a serialized with a create function that looks like this:

class AddressSerializer(serializers.ModelSerializer):
    city_gps = CitySerializer(required=False)
    sector = SectorSerializer(required=False)

    class Meta:
        model = Address
        fields = (..., "sector")

    def create(self, validated_data):
        ...
        sector_dict = validated_data.get("sector", None)   
        sector = None

        if sector_dict and "name" in sector_dict and city_gps:
            if Sector.objects.filter(name=sector_dict["name"], city=city_gps).exists():
                sector = Sector.objects.get(name=sector_dict["name"], city=city_gps)

        # pdb.set_trace()
        if "sector" in validated_data:
            validated_data.pop("sector")
        if "city_gps" in validated_data:
            validated_data.pop("city_gps")

        address = Address.objects.create(sector=sector, city_gps=city_gps, **validated_data)

        return address

The code never touches this function, is_valid() returns False. And the message is

{"sector":{"name":["sector with this name already exists."]}}

I need to be able to create a new address with FK to the already existing sector. How can I achieve that? Any advice will help.

EDIT

The view looks like this:

class ClientProfileAddressCreateView(APIView):
    # throttle_scope = '1persecond'
    renderer_classes = (JSONRenderer,)
    permission_classes = (IsAuthenticated,)

    def post(self, request):

        try:
            client = Client.objects.get(user=request.user)
        except ObjectDoesNotExist:
            return Response({"error": "A client profile for the logged user does not exit"},
                            status=status.HTTP_404_NOT_FOUND)

        address_serialized = AddressSerializer(data=request.data)
        print("address_serialized.is_valid: %s" % address_serialized.is_valid())  # Returns False when unique=True in models
        if address_serialized.is_valid():
            # print("address_serialized: %s" % address_serialized.data)
            address_serialized.save(client=client)
        else:
            return Response(data=address_serialized.errors, status=status.HTTP_400_BAD_REQUEST)

        return Response(data=address_serialized.data, status=status.HTTP_201_CREATED)

回答1:

This is a known issue with nested serializers and unique constraints.

Really awesome thing to always do is actually print the Serializer - that can give you a lot of extra info.

When you have a json like this:

{
    "Sector": {
        "name": "Sector XYZ"
    },
    "address_line_one": “Some Random Address”
}

Django REST framework does not know whether you're creating or getting the Sector object, thus it forces validation on every request.

What you need to do is the following:

class SectorSerializer(serializers.ModelSerializer):
    # Your fields.
    class Meta:
        model = Address
        fields = ("Your Fields",)

        extra_kwargs = {
            'name': {
                'validators': [],
            }
        }

Then to handle validation you would need to redo the create/update part to fit the uniqueness constraint and raise exception/validation error.

I hope this helps.

Helpful links: This SO Answer and Dealing with unique constraints in nested serializers

EDIT :

As per cezar's request: I will add how it might look like to override the create method of the serializer. I have not tried this code, but the logic goes like this.

class SectorSerializer(serializers.ModelSerializer):
    # Your fields.
    class Meta:
        model = Address
        fields = ("Your Fields",)

        extra_kwargs = {
            'name': {
                'validators': [],
            }
        }

    def create(self, validated_data):
        raise_errors_on_nested_writes('create', self, validated_data)

        ModelClass = self.Meta.model

        info = model_meta.get_field_info(ModelClass)
        many_to_many = {}
        for field_name, relation_info in info.relations.items():
            if relation_info.to_many and (field_name in validated_data):
                many_to_many[field_name] = validated_data.pop(field_name)

        # FIELD CHECK
        your_field = validated_data.get("your_field","") # or validated_data["your_field"] 
        try:
            YourModel.objects.filter(your_check=your_field).get()
            raise ValidationError("Your error")
        except YourModel.DoesNotExist:
            # if it doesn't exist it means that no model containing that field exists so pass it. You can use YourQuerySet.exists() but then the logic changes
            pass

        try:
            instance = ModelClass.objects.create(**validated_data)
        except TypeError:
            tb = traceback.format_exc()
            msg = (
                'Got a `TypeError` when calling `%s.objects.create()`. '
                'This may be because you have a writable field on the '
                'serializer class that is not a valid argument to '
                '`%s.objects.create()`. You may need to make the field '
                'read-only, or override the %s.create() method to handle '
                'this correctly.\nOriginal exception was:\n %s' %
                (
                    ModelClass.__name__,
                    ModelClass.__name__,
                    self.__class__.__name__,
                    tb
                )
            )
            raise TypeError(msg)

        # Save many-to-many relationships after the instance is created.
        if many_to_many:
            for field_name, value in many_to_many.items():
                field = getattr(instance, field_name)
                field.set(value)

        return instance