Repository: encode/django-rest-framework

Issue:
PATCH with no data is updating ListField values on ModelSerializer instead of leaving them alone
I've got a model serializer with two ListFields and an email field in it, something like this

```
class MySettingsSerializer(ModelSerializer):
    location_codes = serializers.ListField(child=serializers.CharField(), 
        required=False)
    permissions = serializers.ListField(child=serializers.CharField(), 
        required=False )

    class Meta:
        model = MySettings
        fields = ('id', 'email', 'location_codes', 'permissions' )
```

I've also got a view class that uses UpdateModelMixin to provide an update method.

When I do a PATCH to to my view, the expected behavior is that only the values that are sent will be changed.  For example, PATCH with
{"permissions": [ "change_mymodel" ] }
should change the permissions field on the MySettings object, and not change any of the other fields.  This works as expected.  

However, if I do a PATCH with no data at all, both the permissions and locations_codes are set to empty lists.  The other field (email) is not changed, which leads me to believe that there is bug related to identifying if a ListField should be updated during a PATCH.

Note that this issue cannot be triggered by the HTML form interface by PATCHING a {} -- I can only see it by using a client like Postman and doing a PATCH with no content.



Discussion:
Sounds valid - it'd be good to pull together a more minimal example case that uses a regular serializer rather than a model serializer - that'd help us progress the issue.

Hello from DjangoCon sprints. Working on proof of concept solution for this issue. Should have a PR to you in a few days.

@adamsc64 Welcome onboard ! It would be wonderfull to start with a failing test case first in order to prove the issue and make sure the fix actually fixes the issue.


Resolving patch:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1262,14 +1262,13 @@ def __init__(self, *args, **kwargs):
         super(MultipleChoiceField, self).__init__(*args, **kwargs)
 
     def get_value(self, dictionary):
+        if self.field_name not in dictionary:
+            if getattr(self.root, 'partial', False):
+                return empty
         # We override the default field access in order to support
         # lists in HTML forms.
         if html.is_html_input(dictionary):
-            ret = dictionary.getlist(self.field_name)
-            if getattr(self.root, 'partial', False) and not ret:
-                ret = empty
-            return ret
-
+            return dictionary.getlist(self.field_name)
         return dictionary.get(self.field_name, empty)
 
     def to_internal_value(self, data):
@@ -1416,6 +1415,9 @@ def __init__(self, *args, **kwargs):
         self.child.bind(field_name='', parent=self)
 
     def get_value(self, dictionary):
+        if self.field_name not in dictionary:
+            if getattr(self.root, 'partial', False):
+                return empty
         # We override the default field access in order to support
         # lists in HTML forms.
         if html.is_html_input(dictionary):


Test patch:
diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py
--- a/tests/test_serializer_lists.py
+++ b/tests/test_serializer_lists.py
@@ -289,3 +289,32 @@ class Meta:
         serializer = TestSerializer(data=[], many=True)
         assert not serializer.is_valid()
         assert serializer.errors == {'non_field_errors': ['Non field error']}
+
+
+class TestSerializerPartialUsage:
+    """
+    When not submitting key for list fields or multiple choice, partial
+    serialization should result in an empty state (key not there), not
+    an empty list.
+
+    Regression test for Github issue #2761.
+    """
+    def test_partial_listfield(self):
+        class ListSerializer(serializers.Serializer):
+            listdata = serializers.ListField()
+        serializer = ListSerializer(data=MultiValueDict(), partial=True)
+        result = serializer.to_internal_value(data={})
+        assert "listdata" not in result
+        assert serializer.is_valid()
+        assert serializer.validated_data == {}
+        assert serializer.errors == {}
+
+    def test_partial_multiplechoice(self):
+        class MultipleChoiceSerializer(serializers.Serializer):
+            multiplechoice = serializers.MultipleChoiceField(choices=[1, 2, 3])
+        serializer = MultipleChoiceSerializer(data=MultiValueDict(), partial=True)
+        result = serializer.to_internal_value(data={})
+        assert "multiplechoice" not in result
+        assert serializer.is_valid()
+        assert serializer.validated_data == {}
+        assert serializer.errors == {}