Repository: django-json-api/django-rest-framework-json-api

Issue:
remove ModelViewSet PUT/update() method?
DJA uses DRF's viewsets.ModelViewset which includes both PUT (update()) and PATCH (partial_update()) methods. However,  the JSONAPI spec only defines [PATCH](https://jsonapi.org/format/#crud-updating). DJA allows the PUT method. This becomes more apparent when using the new DRF openapi schema generator since it wants to document the PUT method. While DJA implements PUT identically to PATCH, it might be better to not expose this method as the PUT [semantics](https://tools.ietf.org/html/rfc7231#section-4.3.4) defines a full replacement, which DJA's PUT does not do, since it's just implementing [PATCH](https://tools.ietf.org/html/rfc5789).

I'm not sure exactly where the PUT method gets defined but I think the change is to replace DRF's ModelViewSet with a DJA-specific version that replaces
DRF's `mixins.UpdateModelMixin` with one that doesn't define it. 

https://github.com/django-json-api/django-rest-framework-json-api/blob/de7021f9e011615ce8b65d0cb38227c6c12721b6/rest_framework_json_api/views.py#L189-L192

You can see this by doing an `OPTIONS http://localhost:8000/blogs/1` as in this example:
```json
{
    "data": {
        "name": "Blog Instance",
        "description": "",
        "renders": [
            "application/vnd.api+json",
            "text/html"
        ],
        "parses": [
            "application/vnd.api+json",
            "application/x-www-form-urlencoded",
            "multipart/form-data"
        ],
        "allowed_methods": [
            "GET",
            "PUT",
            "PATCH",
            "DELETE",
            "HEAD",
            "OPTIONS"
        ],
        "actions": {
            "PUT": {
                "name": {
                    "type": "String",
                    "required": true,
                    "read_only": false,
                    "write_only": false,
                    "label": "Name",
                    "max_length": 100
                },
                "tags": {
                    "type": "GenericField",
                    "relationship_type": "OneToMany",
                    "relationship_resource": "taggedItem",
                    "required": false,
                    "read_only": true,
                    "write_only": false,
                    "label": "Tags",
                    "child": {
                        "type": "Serializer",
                        "required": false,
                        "read_only": true,
                        "write_only": false,
                        "children": {
                            "tag": {
                                "type": "Slug",
                                "required": true,
                                "read_only": false,
                                "write_only": false,
                                "label": "Tag",
                                "max_length": 50
                            }
                        }
                    }
                },
                "copyright": {
                    "type": "GenericField",
                    "required": false,
                    "read_only": true,
                    "write_only": false,
                    "label": "Copyright"
                }
            }
        }
    }
}
```


Discussion:
I agree this is a bug as it doesn't follow the specification. Also see [FAQ](https://jsonapi.org/faq/#wheres-put).

I haven't tested this but would it not work if we overwrite `update` and raise a `AttributeError` when partial in kwargs is False?

Something like this:
```python
def update(self, request, *args, **kwargs):
  if not kwargs.get('partial', False):
    raise AttributeError()
  return super().update(self, request, *args, **kwargs)
```

Needs to be tested whether this works. 

Resolving patch:
diff --git a/example/views.py b/example/views.py
--- a/example/views.py
+++ b/example/views.py
@@ -2,7 +2,6 @@
 import rest_framework.parsers
 import rest_framework.renderers
 from django_filters import rest_framework as filters
-from rest_framework import viewsets
 from rest_framework.filters import SearchFilter
 
 import rest_framework_json_api.metadata
@@ -42,7 +41,7 @@ def get_object(self):
         return super(BlogViewSet, self).get_object()
 
 
-class DRFBlogViewSet(viewsets.ModelViewSet):
+class DRFBlogViewSet(ModelViewSet):
     queryset = Blog.objects.all()
     serializer_class = BlogDRFSerializer
     lookup_url_kwarg = 'entry_pk'
@@ -105,7 +104,7 @@ def get_object(self):
         return super(EntryViewSet, self).get_object()
 
 
-class DRFEntryViewSet(viewsets.ModelViewSet):
+class DRFEntryViewSet(ModelViewSet):
     queryset = Entry.objects.all()
     serializer_class = EntryDRFSerializers
     lookup_url_kwarg = 'entry_pk'
diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py
--- a/rest_framework_json_api/views.py
+++ b/rest_framework_json_api/views.py
@@ -243,13 +243,13 @@ class ModelViewSet(AutoPrefetchMixin,
                    PreloadIncludesMixin,
                    RelatedMixin,
                    viewsets.ModelViewSet):
-    pass
+    http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
 
 
 class ReadOnlyModelViewSet(AutoPrefetchMixin,
                            RelatedMixin,
                            viewsets.ReadOnlyModelViewSet):
-    pass
+    http_method_names = ['get', 'head', 'options']
 
 
 class RelationshipView(generics.GenericAPIView):
@@ -257,6 +257,7 @@ class RelationshipView(generics.GenericAPIView):
     self_link_view_name = None
     related_link_view_name = None
     field_name_mapping = {}
+    http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
 
     def get_serializer_class(self):
         if getattr(self, 'action', False) is None:


Test patch:
diff --git a/example/tests/integration/test_polymorphism.py b/example/tests/integration/test_polymorphism.py
--- a/example/tests/integration/test_polymorphism.py
+++ b/example/tests/integration/test_polymorphism.py
@@ -185,8 +185,8 @@ def test_polymorphism_relations_update(single_company, research_project_factory,
         "type": "researchProjects",
         "id": research_project.pk
     }
-    response = client.put(reverse("company-detail", kwargs={'pk': single_company.pk}),
-                          data=content)
+    response = client.patch(reverse("company-detail", kwargs={'pk': single_company.pk}),
+                            data=content)
     assert response.status_code == 200
     content = response.json()
     assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "researchProjects"
@@ -194,18 +194,33 @@ def test_polymorphism_relations_update(single_company, research_project_factory,
         research_project.pk
 
 
-def test_invalid_type_on_polymorphic_relation(single_company, research_project_factory, client):
+def test_polymorphism_relations_put_405(single_company, research_project_factory, client):
     response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk}))
     content = response.json()
     assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects"
 
     research_project = research_project_factory()
     content["data"]["relationships"]["currentProject"]["data"] = {
-        "type": "invalidProjects",
+        "type": "researchProjects",
         "id": research_project.pk
     }
     response = client.put(reverse("company-detail", kwargs={'pk': single_company.pk}),
                           data=content)
+    assert response.status_code == 405
+
+
+def test_invalid_type_on_polymorphic_relation(single_company, research_project_factory, client):
+    response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk}))
+    content = response.json()
+    assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects"
+
+    research_project = research_project_factory()
+    content["data"]["relationships"]["currentProject"]["data"] = {
+        "type": "invalidProjects",
+        "id": research_project.pk
+    }
+    response = client.patch(reverse("company-detail", kwargs={'pk': single_company.pk}),
+                            data=content)
     assert response.status_code == 409
     content = response.json()
     assert len(content["errors"]) == 1