I'm trying to write a test in which an object is updated using patch.
class Search(models.Model):
id_search = models.AutoField(primary_key=True)
id_user = models.IntegerField(null=False)
.
.
archive = models.BooleanField(default=False)
def test_archive_search(self):
user = User(id_user=75720912,
login='Client:75720912',
)
user.save()
search = Search(
id_user=75720912,
.
.
archive=False
)
search.save()
url = reverse('search-update', kwargs={'id_search':1})
data = {'archive': True}
response = self.client.patch(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
url(r'^search-update/(?P<id_search>\d+)$', SearchUpdateView.as_view(), name='search-update')
class SearchUpdateView(generics.UpdateAPIView):
serializer_class = SearchSerializer
def get_object(self,id_search):
return Search.objects.get(id_search=id_search)
def patch(self, request):
id_search = self.request.query_params.get('id_search', None)
search_object = self.get_object(id_search=id_search)
serializer = SearchSerializer(search_object, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.data, status=status.HTTP_400_BAD_REQUEST)
and get this error:
TypeError: patch() got an unexpected keyword argument 'id_search'
Interesting thing is that when the url was:
url(r'^search-update/$', SearchUpdateView.as_view(), name='search-update')
SearchUpdateView worked properly with given query params.
EDIT
I discovered that passing id_search to patch in view solves this problem when it comes to test, but it spoils working view.
class SearchUpdateView(generics.UpdateAPIView):
serializer_class = SearchSerializer
def get_object(self, id_search):
return Search.objects.get(id_search=id_search)
def patch(self, request, id_search):
#id_search = self.request.query_params.get('id_search', None)
search_object = self.get_object(id_search=id_search)
serializer = SearchSerializer(search_object, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.data, status=status.HTTP_400_BAD_REQUEST)
Still I've got no idea how to bring it together.
if you define the url that way, the patch method will get the id_search param as a keyword argument, as the error says.
Instead, you are retrieving it as if it came as a query param, i.e. not part of the url path but as search-update?id_search=.
Given you are passing None as a default when getting it, it works when you omit it.
So choose which way you want to go.
In case the url definition is correct, then add the id_search argument to the signature of the patch method
and remove the code that retrieves it manually.
Or do both, as suggested in the comments above, by assigning a default value of None to the argument and retrieving it from the request if it is not part of the path
Related
I have this code of 2 views in Django. You will notice that each REST API call has a verify_login() function call that ensures that the request contains a verified JWT token. I'm wondering if there's a better way to implement this so that I don't have to have these lines specifically in every REST endpoint
verify_response = verify_login(request)
if verify_response not None:
return verify_response
I'm trying to follow the D.R.Y. (Do Not Repeat Yourself) principle of coding. It'd be nice if there was a cleaner way to represent this. I thought about maybe creating a module extending APIView that automatically has this and then all my Views extend that, but runs into the issue of having to call super().API_REQUEST() and then having to do the same if-statement check to see if it's None or not.
class PostView(APIView):
"""
View for Post object
* requires token authentication
"""
# Create post
#swagger_auto_schema(
request_body=PostSerializer,
operation_description="Create a post object"
)
def post(self, request):
verify_response = verify_login(request)
if verify_response not None:
return verify_response
serializer = PostSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# get all posts
#swagger_auto_schema(
operation_description="Get all posts from the DB"
)
def get(self, request):
verify_response = verify_login(request)
if verify_response not None:
return verify_response
posts = Post.objects.all()
serializer = PostSerializer(posts, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
You can use authentication classes alongside permission classes. If you want the authentication check to happen for all the APIs of your application, put your classes in settings.REST_FRAMEWORK. If you want it for specific APIView's, put them in permission_classes & authentication_classes class variables. Check out the doc for more details.
Example,
class JWTAuthenticataion(BaseAuthentication):
def authenticate(self, request):
... # write your JWT implementation here
# settings.py:
REST_FRAMEWORK = {
...
"DEFAULT_AUTHENTICATION_CLASSES": (
"path.to.JWTAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated",
)
}
# or
# api.py
class YourAPIView(APIView):
permission_classes = (IsAuthenticated, )
authentication_classes = (JWTAuthentication, )
...
How can I validate an additional arguments passed like this:
class MyViewSet(MultiSerializerViewSet):
# some stuff
def perform_create(self, serializer):
serializer.save(creator=self.request.user)
How can I validate a creator in the serializer?
You can not validate fields passed as arguments to serializer.save() method, they will only be available in create method of the serializer, and I suggest not to run validations there. What I do in these kind of situations is, I override the create method of the viewset, and add extra parameters to the data I pass to the serializer.
class MyViewSet(MultiSerializerViewSet):
def create(self, request, *args, **kwargs):
request_data = request.data
request_data['creator'] = self.user.id
serializer = self.get_serializer(data=request_data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
With this setup, you also need to add creator as a serializer field. With this, the field will be alailable in validtion flow.
Django reset framework has is_valid() method validate serializer
for e.g.
serializer = YourSerializer(data={'sample1': 'foobar', 'sample2': 'baz'})
serializer.is_valid()
# False
serializer.errors
# {'sample1': ['Some error.'], 'sample2': ['Some error.']}
refer this
Today I dig into django-rest-auth package a bit. And I have no idea what context={'request': self.request} for in get_response and post function.
They say context parameter is for including extra context to serializer. But below the codes, it seems not necessary to put context parameter. Is there something I have missed?
context: http://www.django-rest-framework.org/api-guide/serializers/
django-rest-auth : https://github.com/Tivix/django-rest-auth/blob/master/rest_auth/views.py
def get_response(self):
serializer_class = self.get_response_serializer()
if getattr(settings, 'REST_USE_JWT', False):
data = {
'user': self.user,
'token': self.token
}
serializer = serializer_class(instance=data,
context={'request': self.request})
else:
serializer = serializer_class(instance=self.token,
context={'request': self.request})
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, *args, **kwargs):
self.request = request
self.serializer = self.get_serializer(data=self.request.data,
context={'request': request})
self.serializer.is_valid(raise_exception=True)
self.login()
return self.get_response()
Sometimes you need request's data inside serializer method. For this case you can provide request to serializer's context. For example if you look into PasswordResetSerializer you'll see in save method use_https option which calculated based on the request passed with context argument:
def save(self):
request = self.context.get('request')
# Set some values to trigger the send_email method.
opts = {
'use_https': request.is_secure(),
'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'),
'request': request,
}
Also you can check if user is authenticated or not and depends on it return one data or another on serializer level.
I'm using django.rest_framework. I have a get_or_create method for a particular view,
class LocationView(views.APIView):
def get_or_create(self, request):
try:
location = Location.objects.get(country=request.data.get("country"), city=request.data.get("city"))
Response(location, status=status.HTTP_200_OK)
except Location.DoesNotExist:
serializer = LocationSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
This is the location Model,
class Location(models.Model):
country = models.CharField(max_length=255)
city = models.CharField(max_length=255, unique=True)
latitude = models.CharField(max_length=255)
longitude = models.CharField(max_length=255)
class Meta:
unique_together = ('country', 'city')
This is my url,
url(r'^location/$', LocationView.as_view(), name='location'),
When I call this endpoint in the following way,
http://127.0.0.1:8000/api/v1/bouncer/location/?country=USA&&city=Sunnyvale&&latitude=122.0363&&longitude=37.3688
This is what I get,
{
"detail": "Method \"GET\" not allowed."
}
What am I missing here.
The Method not allowed error is because, it searches for a get() method inside your API class, and it couldn't find a one.
The general format of the API class is as below
class LocationView(views.APIView):
def get(self, request):
#do something with 'GET' method
return Response("some data")
def post(self, request):
#do something with 'POST' method
return Response("some data")
If you want to invoke the get_or_create() method at some point, you can do it as any other methods,
class LocationView(views.APIView):
def get_or_create(self, request):
# do some "get or create" stuff
return "some data"
def get(self, request):
if condition:
self.get_or_create(request)
# do some stuff
return Response(" some special data related to get or create")
return Response("some data")
You need to provide separate methods for get and post. If your get method also creates an instance, then you can just call your current get_or_create inside both get and post methods.
class LocationView(views.APIView):
def get_or_create(self, request):
# your current definition here
def get(self, request):
return self.get_or_create(request)
def post(self, request):
return self.get_or_create(request)
For my situation, I was sending a request properly as my view would expect, a POST. But the issue was on http/s. I had set a permanent redirect, from http to https on my app, and so sending a POST to the http version resulted in a GET somehow when the server was redirecting.
TL;DR
HTTP instead of HTTPS
I'm making a Django application, where I am sending requests through rest API. I am using POST and GET and all works pretty well, but when I am trying to use PATCH (as I must firstly upload field "start_time", and then add field "time"), I'm getting following error:
match = time_re.match(value)
TypeError: expected string or buffer
Surely it is about views.py, but I cannot find clear recipe how to do that, thus I don't know where I am wrong.
Thank you.
Views.py
...
elif request.method = 'PATCH':
serializer = TABLESerializer(data=request.data, partial=True)
if serializer.is_valid():
obj = TABLE.objects.get(start_time=request.data['start_time'])
obj.time = serializer['time']
obj.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Serializers.py
class TABLESerilizer(serializers.serializer):
start_time = serializers.DateTimeField(format = None, allow_null=True)
time = serializers.TimeField(format=None, required=False)
models.py
class TABLE(models.Model):
start_time=models.DateTimeField(primary_key=True)
time = models.TimeField(null= True, blank= True, default = '00:00:00')
format - A string representing the output format. If not specified,
this defaults to the same value as the TIME_FORMAT settings key, which
will be 'iso-8601' unless set.
But in your case you set it to None. we get error when python try to parse the data into time object with format None format must me like HH:MM:SS, etc.
update your serializer like below
from rest_framework.settings import api_settings
class TABLESerilizer(serializers.serializer):
start_time = serializers.DateTimeField(format=api_settings.TIME_FORMAT)
time = serializers.TimeField(format=api_settings.TIME_FORMAT)
References: http://www.django-rest-framework.org/api-guide/fields/#timefield
https://github.com/django/django/blob/master/django/utils/dateparse.py#L80