Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OneToOne field partial_update with unique tel get unique error. #34

Closed
yinkh opened this issue Mar 12, 2018 · 3 comments
Closed

OneToOne field partial_update with unique tel get unique error. #34

yinkh opened this issue Mar 12, 2018 · 3 comments

Comments

@yinkh
Copy link

yinkh commented Mar 12, 2018

models.py:

class User(AbstractBaseUser, PermissionsMixin):
    tel = CharNullField(max_length=20,
                        unique=True,
                        null=True,
                        error_messages={
                            'unique': "The number is registered.",
                        },
                        verbose_name='tel')

class Staff(models.Model):
    user = models.OneToOneField('user.User',
                                null=True,
                                on_delete=models.CASCADE,
                                verbose_name='user ')

serializers.py:

class StaffUserCreateSerializer(ModelSerializer):

    def create(self, validated_data):
        groups = validated_data.pop('groups')
        instance = User.objects.create_user(**validated_data)
        instance.groups.add(*groups)
        return instance

    def update(self, instance, validated_data):
        password = validated_data.pop('password')
        super(StaffUserCreateSerializer, self).update(instance, validated_data)
        instance.set_password(password)
        instance.save()
        return instance

    def validate_password(self, value):
        password_validation.validate_password(value)
        return value

    class Meta:
        model = User
        fields = ('id', 'name', 'tel', 'email', 'password', 'is_active', 'is_staff', 'groups')

class StaffModifySerializer(WritableNestedModelSerializer):
    user = StaffUserCreateSerializer(label='User', partial=True)

    class Meta:
        model = Staff
        fields = ('user', 'job_state', 'bank_no', 'social_security_no', 'on_job')

tests.py:

class StaffTests(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user(tel='18094213193', password='123456')
        self.user.user_permissions.add(*get_model_permission(Staff))
        token, _ = Token.objects.get_or_create(user=self.user)
        self.access_token = token.access_token
        self.group = Group.objects.create(name='test')
        self.data = {
            'user': {'name': 'YKH', 'tel': '17749503263',  'password': 'YKH123456']},
        }

    def test_staff_update(self):
        """update"""
        user = User.objects.create_user(tel='17749503263', password='YKH123456')
        instance = Staff.objects.create(user=user, no='17749503263')
        print('original:', instance.user.id)
        url = reverse('staff-detail', kwargs={'pk': instance.id})
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.access_token)
        data = {
            'user': {'id': 2, 'name': 'YKH1', 'tel': '17749503263', 'password': 'YKH654321'}
        }
        response = self.client.patch(url, data, format='json')
        print('update', json.dumps(response.data, ensure_ascii=False, indent=2))
        self.assertEqual(response.status_code, status.HTTP_200_OK)

when I run test_staff_update which update nested user with same tel,I get the error data": { "user": { "tel": [ "The number is registered." ] }, I know when use partial_update method you shouldn't pass the data do not need change,but i don't want do it in front end which use VUE.And I know this problem is cause by when get child field user,DRF don't pass instance kwargs to it.So i change my StaffModifySerializer to:

class StaffModifySerializer(WritableNestedModelSerializer):
    user = StaffUserCreateSerializer(label='user', partial=True)

    class Meta:
        model = Staff
        fields = ('user', 'job_state', 'bank_no', 'social_security_no', 'on_job')

    def __init__(self, *args, **kwargs):
        super(StaffModifySerializer, self).__init__(*args, **kwargs)
        import copy
        initial_data = copy.deepcopy(self.initial_data)
        relations, reverse_relations = self._extract_relations(initial_data)

        for field_name, (field, field_source) in relations.items():
            data = self.initial_data[field_name]
            model_class = field.Meta.model
            pk = self._get_related_pk(data, model_class)
            try:
                obj = model_class.objects.get(pk=pk, )

                self.fields[field_name] = self._get_serializer_for_field(
                    self.fields[field_name], instance=obj, data=data)
            except (model_class.DoesNotExist, ValueError):
                pass

the king mind is replace field user(StaffUserCreateSerializer(partial=True)) to new field user(StaffUserCreateSerializer(instance=User_by_id, partial=True)), I want to witre it inside WritableNestedModelSerializer but get error when use nested many_to_many,when I‘m free,I will try to write PR.

@ruscoder
Copy link
Member

ruscoder commented Apr 20, 2018

@yinkanghong Hello! I'm not sure, but it could be possibly fixed in 4.0.2. It will be great if you can check your code with the latest version of the package and leave your feedback.

@yinkh
Copy link
Author

yinkh commented Apr 23, 2018

My drf-nested-serializer version is 0.4.1,I updated it to 0.4.2 which is latest version now,and this issue still exist.I will try to write a PR this week.

@yinkh
Copy link
Author

yinkh commented Apr 23, 2018

I find this is a issue for DRF(issue2403issue2996),In DRF validators/BaseUniqueForValidator this is a method exclude_current_instance code is:

def exclude_current_instance(self, attrs, queryset):
    """
    If an instance is being updated, then do not include
    that instance itself as a uniqueness conflict.
    """
    if self.instance is not None:
        return queryset.exclude(pk=self.instance.pk)
    return queryset

,when use drf-writable-nested,the validation is handle by DRF,the ModelSerializer or ListSerializer's child(which is nested ModelSerializer) only have id/pk in initial_data,but don't has instance object.So if we convert id/pk to instance object in ModelSerializer this issue will be solved.I find there ways to do this:

First way is convert id/pk to instance:

class StaffUserCreateSerializer(ModelSerializer):

    def to_internal_value(self, data):
        if 'pk' in data and 'pk' in self.fields:
            print(data)
            try:
                obj_id = self.fields['pk'].to_internal_value(data['pk'])
            except serializers.ValidationError as exc:
                raise serializers.ValidationError({'pk': exc.detail})
            self.instance = self.Meta.model.objects.get(id=obj_id)
        return super(StaffUserCreateSerializer, self).to_internal_value(data)

    class Meta:
        model = User
        fields = ('pk', 'name', 'tel',)

class StaffModifySerializer(WritableNestedModelSerializer):
    user = StaffUserCreateSerializer(label='User', partial=True)

    class Meta:
        model = Staff
        fields = ('user', 'job_state', 'bank_no', 'social_security_no', 'on_job')

Second way is change the queryset to exclued the pk:

class StaffUserCreateSerializer(ModelSerializer):

    def to_internal_value(self, data):
        if 'pk' in data and 'pk' in self.fields:
            print(data)
            try:
                obj_id = self.fields['pk'].to_internal_value(data['pk'])
            except serializers.ValidationError as exc:
                raise serializers.ValidationError({'pk': exc.detail})
            for field in self.fields.values():
                for validator in field.validators:
                    from rest_framework.validators import UniqueValidator
                    if type(validator) == UniqueValidator:
                        # Exclude id from queryset for checking uniqueness
                        validator.queryset = validator.queryset.exclude(id=obj_id)
        return super(StaffUserCreateSerializer, self).to_internal_value(data)

    class Meta:
        model = User
        fields = ('pk', 'name', 'tel',)

class StaffModifySerializer(WritableNestedModelSerializer):
    user = StaffUserCreateSerializer(label='User', partial=True)

    class Meta:
        model = Staff
        fields = ('user', 'job_state', 'bank_no', 'social_security_no', 'on_job')

Third way is convert id/pk to instance in WritableNestedModelSerializer:

class StaffUserCreateSerializer(ModelSerializer):
    class Meta:
        model = User
        fields = ('pk', 'name', 'tel',)

class StaffModifySerializer(WritableNestedModelSerializer):
    user = StaffUserCreateSerializer(label='User', partial=True)

   def __init__(self, *args, **kwargs):
        super(StaffModifySerializer, self).__init__(*args, **kwargs)
        import copy
        initial_data = copy.deepcopy(self.initial_data)
        relations, reverse_relations = self._extract_relations(initial_data)

        for field_name, (field, field_source) in relations.items():
            data = self.initial_data[field_name]
            model_class = field.Meta.model
            pk = self._get_related_pk(data, model_class)
            try:
                obj = model_class.objects.get(pk=pk, )

                self.fields[field_name] = self._get_serializer_for_field(
                    self.fields[field_name], instance=obj, data=data)
            except (model_class.DoesNotExist, ValueError):
                pass

    class Meta:
        model = Staff
        fields = ('user', 'job_state', 'bank_no', 'social_security_no', 'on_job')

Third way can't used for many_to_many fields which has unique=True.

I think all of this there solutions is not beautiful enough to write a PR,so i will close this issue.

Thank for this good library,drf-writable-nested really help me a lot. 😄

@yinkh yinkh closed this as completed Apr 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants