restframework, viewset, put & patch (=ruolo di partial)

16 views
Skip to first unread message

Alessandro Dentella

unread,
Nov 11, 2018, 5:18:27 PM11/11/18
to djan...@googlegroups.com
Ciao a tutti,

mi sono appena accorto che DRF si comporta diverso da come avrei
immaginato, vorrei capire se ho in testa un modello scorretto, se ho
qualcosa configurato male o cos'altro.

Immaginiamo un modello semplice con un campo NON obbligatorio:

class User(AbstractUser):
height = models.CharField(max_length=30, null=True, blank=True)

class UserViewSet(ModelViewset):

"""
API endpoint that allows Cv to be viewed or edited.
"""
queryset = models.User.objects.all().order_by('username')
serializer_class = serializers.UserSerializer

class UserSerializer(serializers.ModelSerializer):

class Meta:
model = models.User
fields = '__all__'


ed immaginiamo di avere creato l'utente nicolas con height 122.
Ora faccio una put con tutti i dati *escluso* height.

Mi aspetterei che la successiva get, restituisca il campo height
vuoto, mentre non č cosě. Di fatto secondo me agisce come una PATCH.

Nel viewset i metodi relativi sono update e partial_update, la cui
differenza č che chiamano il costruttore del serialier con il
parametro partial=True|False che mi pare agisca solo sulla
validazione [1], quindi č normale che non "vuoti" il campo che non
viene passato in argomento...

Sbaglio a pensare che PUT dovrebbe operare una sostituzione completa
dell'oggetto?

sandro
*:-)





[1] https://www.django-rest-framework.org/api-guide/serializers/#partial-updates

Raffaele Salmaso

unread,
Nov 11, 2018, 6:26:44 PM11/11/18
to djan...@googlegroups.com
Non credo di seguirti

$ curl -H "Content-Type: application/json" "http://localhost:8088/api/users/" -X POST -v -d '{"username": "nicolas", "password": "secretpwd"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8088 (#0)
> POST /api/users/ HTTP/1.1
> Host: localhost:8088
> User-Agent: curl/7.58.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 48
* upload completely sent off: 48 out of 48 bytes
< HTTP/1.1 201 Created
< Date: Sun, 11 Nov 2018 23:13:40 GMT
< Server: WSGIServer/0.2 CPython/3.6.6
< Content-Type: application/json
< Allow: GET, POST, HEAD, OPTIONS
< X-Frame-Options: SAMEORIGIN
< Vary: Accept-Language, Cookie, Origin
< Content-Language: en
< Content-Length: 264
* Connection #0 to host localhost left intact
{"id":7,"password":"secretpwd","last_login":null,"is_superuser":false,"username":"nicolas","first_name":"","last_name":"","email":"","is_staff":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:00","height":null,"groups":[],"user_permissions":[]}

$ curl -H "Content-Type: application/json" "http://localhost:8088/api/users/7/" -X PUT -d '{"id":7,"password":"secretpwd","last_login":null,"is_superuser":false,"username":"nicolas","first_name":"","last_name":"","email":"","is_staff":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:00"}'
{"id":7,"password":"secretpwd","last_login":null,"is_superuser":false,"username":"nicolas","first_name":"","last_name":"","email":"","is_staff":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:00","height":null,"groups":[],"user_permissions":[]}

$ curl -H "Content-Type: application/json" "http://localhost:8088/api/users/7/" -X POST -d '{"id":7,"password":"secretpwd","last_login":null,"is_superuser":false,"username":"nicolas","first_name":"","last_name":"","email":"","is_staff":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:00"}'
{"detail":"Method \"POST\" not allowed."}

$ curl -H "Content-Type: application/json" "http://localhost:8088/api/users/7/" -X PATCH -d '{"id":7,"password":"secretpwd","last_login":null,"is_superuser":false,"username":"nicolas","first_name":"","last_name":"","email":"","is_staff":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:00"}'
{"id":7,"password":"secretpwd","last_login":null,"is_superuser":false,"username":"nicolas","first_name":"","last_name":"","email":"","is_staff":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:00","height":null,"groups":[],"user_permissions":[]}

$ curl -H "Content-Type: application/json" "http://localhost:8088/api/users/7/" -X GET
{"id":7,"password":"secretpwd","last_login":null,"is_superuser":false,"username":"nicolas","first_name":"","last_name":"","email":"","is_staff":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:00","height":null,"groups":[],"user_permissions":[]}

$ cat models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _


class User(AbstractUser):
    height = models.CharField(max_length=30, null=True, blank=True)

    class Meta:
        ordering = ('id',)
        verbose_name = _('user')
        verbose_name_plural = _('users')

$ cat views.py
from rest_framework import serializers, viewsets

from .models import User


class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = '__all__'


class UserViewSet(viewsets.ModelViewSet):

    """
    API endpoint that allows Cv to be viewed or edited.
    """

    queryset = User.objects.all().order_by('username')
    serializer_class = UserSerializer

$ cat urls.py
from django.urls import include, re_path as url
from rest_framework import routers

from . import views

router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet, base_name='users')

urlpatterns = [
    url(r"^", include(router.urls)),
]

Sicuro di non avere qualche cosa che manipoli il dato?
Poi che db stai usado? (domanda in effetti un po' strana, visto che di db ce ne solo uno ;) )
--

Raffaele Salmaso

unread,
Nov 11, 2018, 6:42:49 PM11/11/18
to djan...@googlegroups.com
ah, django 2.1.3 e drf 3.9.0 (e postgresql 😁)
 

Alessandro Dentella

unread,
Nov 11, 2018, 7:24:22 PM11/11/18
to djan...@googlegroups.com
Ciao Raffaele,

(una certezza la tua risposta *;-)
> "[2]http://localhost:8088/api/users/" -X POST -v -d '{"username":
> "[3]http://localhost:8088/api/users/7/" -X PUT -d
> '{"id":7,"password":"secretpwd","last_login":null,"is_superuser":false,
> "username":"nicolas","first_name":"","last_name":"","email":"","is_staf
> f":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:
> 00"}'
> {"id":7,"password":"secretpwd","last_login":null,"is_superuser":false,"
> username":"nicolas","first_name":"","last_name":"","email":"","is_staff
> ":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:0
> 0","height":null,"groups":[],"user_permissions":[]}
> $ curl -H "Content-Type: application/json"
> "[4]http://localhost:8088/api/users/7/" -X POST -d
> '{"id":7,"password":"secretpwd","last_login":null,"is_superuser":false,
> "username":"nicolas","first_name":"","last_name":"","email":"","is_staf
> f":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:
> 00"}'
> {"detail":"Method \"POST\" not allowed."}
> $ curl -H "Content-Type: application/json"
> "[5]http://localhost:8088/api/users/7/" -X PATCH -d
> '{"id":7,"password":"secretpwd","last_login":null,"is_superuser":false,
> "username":"nicolas","first_name":"","last_name":"","email":"","is_staf
> f":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:
> 00"}'
> {"id":7,"password":"secretpwd","last_login":null,"is_superuser":false,"
> username":"nicolas","first_name":"","last_name":"","email":"","is_staff
> ":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:0
> 0","height":null,"groups":[],"user_permissions":[]}
> $ curl -H "Content-Type: application/json"
> "[6]http://localhost:8088/api/users/7/" -X GET
Sicuro. Ho tutto come te. Il fatto che è tu sia nella PATCH che nella
PUT metti tutti i dati, mentre io suppongo (supponevo?) che quello che
non passo sia preso per nullo in una PUT:


sandro@bluff:~$ curl -s -H "Content-Type: application/json" "http://localhost:8000/api/user/3/" | python -m json.tool{
"height": "122",
"country_id": null,
"date_joined": "2018-11-11T23:13:40.458283",
"email": "",
"first_name": "",
"id": 3,
"is_active": true,
"is_staff": false,
"is_superuser": false,
"last_login": null,
"last_name": "",
"username": "nicolas"
}

sandro@bluff:~$ curl -s -H "Content-Type: application/json" "http://localhost:8000/api/user/3/" -X PATCH -d '{"height":122}'| python -m json.tool
{
"height": "122",
"country_id": null,
"date_joined": "2018-11-11T23:13:40.458283",
"email": "",
"first_name": "",
"id": 3,
"is_active": true,
"is_staff": false,
"is_superuser": false,
"last_login": null,
"last_name": "",
"username": "nicolas"
}

Questa è la prova di quanto dicevo, da dimostrare che sia sbagliata...
nel payload NON INVIO il valore di height che è opzionale e questo non
viene toccato. Il comportamento quindi diviene il medesimo di PATCH...


sandro@bluff:~$ curl -s -H "Content-Type: application/json" "http://localhost:8000/api/user/3/" -X PUT -d '{"password":"secretpwd","last_login":null,"is_superuser":false,"username":"nicolas","first_name":"","last_name":"","email":"","is_staff":false,"is_active":true,"date_joined":"2018-11-12T00:13:40.458283+01:00"}' | python -m json.tool
{
"height": "122",
"country_id": null,
"date_joined": "2018-11-11T23:13:40.458283",
"email": "",
"first_name": "",
"id": 3,
"is_active": true,
"is_staff": false,
"is_superuser": false,
"last_login": null,
"last_name": "",
"username": "nicolas"
}


sandro
*:-)

Marco De Paoli

unread,
Nov 12, 2018, 12:42:38 AM11/12/18
to djan...@googlegroups.com
Il giorno lun 12 nov 2018 alle ore 01:24 Alessandro Dentella <san...@e-den.it> ha scritto:

Questa è la prova di quanto dicevo, da dimostrare che sia sbagliata...
nel payload NON INVIO il valore di height che è opzionale e questo non
viene toccato. Il comportamento quindi diviene il medesimo di PATCH...

... il che, in effetti, non sembra coerente con l'RFC 

"The existing HTTP PUT method only allows a *complete replacement* of a document."

ed è proprio questo il motivo per cui hanno introdotto PATCH:

This proposal adds a new HTTP method, PATCH, to *modify an existing* HTTP resource.

Marco

Raffaele Salmaso

unread,
Nov 12, 2018, 2:48:26 AM11/12/18
to djan...@googlegroups.com

Quindi sembra di si, che per i field non obbligatori PUT e PATCH si comportano alla stessa maniera ("non lo fornisci, quindi non lo modifico")

   If the target resource does not have a current representation and the
   PUT successfully creates one, then the origin server MUST inform the
   user agent by sending a 201 (Created) response.  If the target
   resource does have a current representation and that representation
   is successfully modified in accordance with the state of the enclosed
   representation, then the origin server MUST send either a 200 (OK) or
   a 204 (No Content) response to indicate successful completion of the
   request.
se posso non dichiarare alcuni campi in una POST perché devo dichiararli in una PUT se non mi interessa modificare il valore?




Reply all
Reply to author
Forward
0 new messages