Update and delete in same Api view without passing id in Url - python

How to perform crud operation in One URL End point in django rest framework?
Currently i am having 2 url end points
url(r'^recipient/$', views.RecipientView.as_view()), # in this APiview im performing get all and post
url(r'^recipient/(?P<pk>[0-9]+)/$', views.RecipientDetail.as_view()), # in this APiview im performing retrieve, update delete.
Now the requirement is i have remove 2nd url and perform all operations in first api view?
I am new to django framework can anyone please help me achieve this?
Below is my code.
View.py
class RecipientView(APIView):
def get(self, request, format=None):
Recipients = Recipient.objects.all()
serializer = RecipientSerializer(Recipients, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = RecipientSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
"""
class RecipientDetail(APIView):
def get_object(self, pk):
try:
return Recipient.objects.get(pk=pk)
except Recipient.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
Recipient = self.get_object(pk)
serializer = RecipientSerializer(Recipient)
return Response(serializer.data)
def put(self, request, pk, format=None):
Recipient = self.get_object(pk)
serializer = RecipientSerializer(Recipient, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None):
Recipient = self.get_object(pk)
Recipient.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
"""
model.py
class Recipient(models.Model):
recipient = models.CharField(max_length=32, blank=False, null=False)
def __str__(self):
"""returns the model as string."""
return self.racipi
ent
serializer.py
class RecipientSerializer(serializers.ModelSerializer):
class Meta:
model = Recipient
fields = '__all__'
I am not able to update and delete in the same view please needed help?

You can avoid passing the ID in the URL with a POST request. Supply the ID and some kind of "action" verb, e.g. action=delete in the body of the request.
That's not considered RESTful though, partly because the HTTP DELETE and PUT verbs perfectly describes the requested operations, but also because POST is considered a non-idempotent method, meaning that the server state will change with each successful request. Being idempotent, duplicate DELETE/PUT (and GET for that matter) requests will leave the server in the same state.
It's not a major hassle to have a second route and view to implement the REST API so it's best to leave it as it is.

Your 2nd URL is receiving a parameters that can be used to fetch data object from the database and then perform any action on that particular instance. If you see the class RecipientDetail, you'll see all the methods are accepting a parameter called pk that relates to the object you want to fetch from database.
But your 1st URL is for generic actions like Create New Object or List All Objects and it is not good to use these endpoints to do instance specific actions.
You can read more about REST API standard enpoints to know details. Here is a reference link:
https://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api

The easiest way is to use DRF's ViewSet. It already provides the basic CRUD operation for you so you can just create the view in something like this:
# views.py
from rest_framework import viewsets
from .models import Recipient
from .serializers import RecipientSerializer
class RecipientViewSet(viewsets.ModelViewSet):
"""
A viewset for viewing and editing recipient instances.
"""
serializer_class = RecipientSerializer
queryset = Recipient.objects.all()
Since we are using ModelViewSet, it already provides actions like get, list, update, etc. as you can see in the documentation.
You can then use routers in your urls.py like this:
# urls.py
from rest_framework.routers import DefaultRouter
from myapp.views import RecipientViewSet
router = DefaultRouter()
router.register(r'recipients', RecipientViewSet)
urlpatterns = router.urls
The url above will generate a url that is like what you wrote in you question:
# Add recipient
POST /recipients/
# Get list of recipients
GET /recipients/
# Get recipient detail
GET /recipients/:recipient_id/
# Update recipient
PUT/PATCH /recipients/:recipient_id/
# Delete recipient
DELETE /recipients/:recipient_id/
Please take note that this is a simplified version and you can even create your own urls pattern with your specified actions.
UPDATE:
Thanks to mhawke for clarification. As what mhawke said in the comment, this may not be what the OP wanted, if you just want to avoid passing the ID in the url, then you can follow mawke's answer, and yes it is not considered RESTful.

Related

Adding a redirect to CreateAPIView

I want to redirect the user to the AddQuestionsView after the user creates a quiz(after adding title).
My CreateQuiz
class CreateQuizzView(CreateAPIView):
serializer_class = CreateQuizSerializer
My serializers.py file
class CreateQuizSerializer(serializers.ModelSerializer):
class Meta:
model = Quizzer
fields = ['title']
def create(self, validated_data):
user = self.context['request'].user
new_quiz = Quizzer.objects.create(
user=user,
**validated_data
)
return new_quiz
Can i add redirect by adding any Mixin or change need to change the GenericView.
An APIView is normally not used by a browser, or at least not directly, hence a redirect makes not much sense. The idea is that some program makes HTTP requests, and thus retrieves a response. Most API handlers will not by default follow a redirect anyway.
You can however make a redirect, by overriding the post method:
from django.shortcuts import redirect
class CreateQuizzView(CreateAPIView):
serializer_class = CreateQuizSerializer
def post(self, *args, **kwargs):
super().post(*args, **kwargs)
return redirect('name-of-the-view')

Rest Framework serializers, forbid users to change others password

I'm creating a simple web app and I cannot find any way to forbid other users from changing your password. This is minimal code:
# serializers.py
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
def create(self, validated_data):
# have to use custom create method because default method calls `User.objects.create()` which doesn't take care of password hashing and other important stuff
return User.objects.create_user(**validated_data)
class Meta:
model = User
fields = ('id', 'username', 'email', 'password')
read_only_fields = ('id', 'username')
# password is set to write_only because I don't want to send it to anybode (even though it's just a hash)
extra_kwargs = {'password': {'write_only': True}}
# views.py
from .serializers import UserSerializer
from rest_framework import generics
from django.contrib.auth.models import User
class UserDetails(generics.RetrieveUpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
I could take care of this myself by using APIView.
# views.py
from .serializers import UserSerializer
from rest_framework.views import APIView
from django.contrib.auth.models import User
class UserDetails(APIView):
def put(self, request, format=None):
serialized = UserSerializer(data=request.DATA)
if not serialized.is_valid():
return # probably HTTP 400 Error code
if request.user.id != serialized.data['id']:
# this if is what I'm trying to achieve
return # probably HTTP 403 Error code
user = User.objects.update(
id=serialized.data['id'],
email=serialized.data['email']
)
if 'password' in request.DATA:
user.set_password(request.DATA['password'])
return # probably HTTP 200 Error code
Unfortunately, that would have caused that scheme generated by rest_framework.schemas.get_schema_view would be incomplete. And I use for communication CoreAPI (which cannot communicate with something that is not described in the scheme) so I cannot do that. I haven't found anything in the official documentation.
This seems to be a too basic problem that has a super easy solution that I missed. Thanks for any ideas or places where to look.
PS: I'm using django2.1 with python3.6
Edit: osobacho's solutions is clean and works like charm. Anyway I also need to allow modifications (let's say of TODO-list) only to creator of that todo list. Thought that solution for password problem would be applicable but it's not.
You can get the user in the request. That way each user will only be able to change their own password.
class UserDetails(generics.RetrieveUpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
def get_object(self):
return self.request.user
For your second question take a look at django_restframework http://www.django-rest-framework.org/api-guide/permissions/ permissions here is an example:
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Instance must have an attribute named `owner`.
return obj.owner == request.user
then you need to add to your view:
permission_classes = (IsOwnerOrReadOnly,)
hope it helps

Use custom function in ModelViewSet with Django Rest Framework

Django 2.0, Python 3.6, Django Rest Framework 3.8
I'm still pretty new to Django Rest Framework, and I'm trying to wrap my head around the logic for using functions in a viewset (and if this is even the correct place to include a function).
Basically, I would like to send out an email when a user posts something to the api in this specific viewset. I tried using the send_mail function, but have been unsuccessful. I have the following class based view:
class SendInviteView(viewsets.ModelViewSet):
queryset = models.Message.objects.all()
serializer_class = serializers.MessageSerializer
#action(methods=['post'], detail=True)
def send_the_mail(self, request):
send_mail(
'Invitation',
'Try our app!',
'exampleemail#gmail.com',
['examplerecipient#gmial.com'],
fail_silently=False,
)
[The Model and Serializer are pretty basic, and I don't think will be required for the context of this problem, basically just an EmailField(). I eventually plan to use the input of that email field to replace examplerecipient#gmail.com, but for now I just want to understand how to add functionality to viewsets]
This results in an error when running python manage.py check
I have my email client set up through sendgrid and am able to successfully send emails to users who ask to have their passwords reset through rest-auth, but I don't understand how sending an email works outside of that context.
Any help is greatly appreciated.
After the discussion, I would came up with the following.
from django.conf import settings
from django.core.mail import send_mail
from django.db import models
from rest_framework import serializers, viewsets, routers, mixins
from rest_framework.response import Response
class Message(models.Model):
sender = models.ForeignKey(settings.AUTH_USER_MODEL)
recipient = models.EmailField()
class MessageSerializer(serializers.ModelSerializer):
message = serializers.CharField(write_only=True)
class Meta:
model = Message
fields = ['recipient', 'message']
def create(self, validated_data):
message = validated_data.pop('message')
message_obj = super().create(validated_data)
send_mail(
'Invitation',
message,
'exampleemail#gmail.com',
[message_obj.recipient]
)
return message_obj
class SendInviteView(mixins.CreateModelMixin, viewsets.GenericViewSet):
serializer_class = MessageSerializer
def perform_create(self, serializer):
serializer.save(sender=self.request.user)
router = routers.DefaultRouter()
router.register('send_invite', SendInviteView, base_name='send_invite')
urlpatterns = router.urls
Let's break things up.
If you want to store sender, you need ForeignKey to User in your model.
For serializer you need to add message field manually because it doesn't exists in your model, but users should submit it. We set it to write-only, because this serializer will be also used to serialize created Message back to user for response, and Message don't have message field. This serializer will also generate field for recipient automatically from Message model.
Then we override create in this serializer, so whenever new Message will be created using it, it will send an email. It calls super().create(..) to actually save Message to database and then sends an email. We use .pop() to remove message from validated_data, because Message doesn't contain such field.
For the view we don't need the whole stuff the ModelViewSet provides. It adds ability to Create, Read, Update and Delete (CRUD) your Message, which is not actually needed. All you need is simple Create which translates to POST in term of HTTP request. GenericViewSet with CreateModelMixin is exactly the thing we want (And actually ModelViewSet just have MORE mixins). The data from user will be validated by serializer and than perform_create method will be invoked. We are passing sender=self.request.user to serializer.save() because we need to save sender into Message, and sender field is not actually in the data, it is the user currently logged-in.
serializer.save() will run our MessageSerializer.create() so we are happy.
Note that this stuff will work only for logged-in users, because we somehow need to populate sender field in database, so it will be correct to add
class SendInviteView(mixins.CreateModelMixin, viewsets.GenericViewSet):
permission_classes = [IsAuthenticated]
....
So only authenticated users can make request.
Hopefully this will clarify things for you. Best regards)
If I'm understood correctly, you could mention the function/method in urls as below,
url(r'some/end/point/', views.SendInviteView.as_view({"post": "send_the_mail"})
Hence, your view be like,
class SendInviteView(viewsets.ModelViewSet):
queryset = models.Message.objects.all()
serializer_class = serializers.MessageSerializer
def send_the_mail(self, request):
recipient = request.data['recipient'] # json array
send_mail(
'Invitation',
'Try our app!',
'exampleemail#gmail.com',
recipient,
fail_silently=False,
)
return Response("mail sent successfully")
Since recipient expects an array, so the POST payload will be like,
{
"recipient": ["mail1#dom.com", "mail2#dom.com", "mail3#dom.com"]
}

Django REST framework - multiple lookup fields?

I have a model that more or less looks like this:
class Starship(models.Model):
id = models.UUIDField(default=uuid4, editable=False, primary_key=True)
name = models.CharField(max_length=128)
hull_no = models.CharField(max_length=12, unique=True)
I have an unremarkable StarshipDetailSerialiser and StarshipListSerialiser (I want to eventually show different fields but for now they're identical), both subclassing serializers.ModelSerializer. It has a HyperlinkedIdentityField that refers back to the (UU)ID, using a home-brew class very similar to the original HyperlinkedIdentityField but with capability to normalise and handle UUIDs:
class StarshipListSerializer(HyperlinkedModelSerializer):
uri = UUIDHyperlinkedIdentityField(view_name='starships:starship-detail', format='html')
class Meta:
model = Starship
fields = ('uri', 'name', 'hull_no')
Finally, there's a list view (a ListAPIView) and a detail view that looks like this:
class StarshipDetail(APIView):
"""
Retrieves a single starship by UUID primary key.
"""
def get_object(self, pk):
try:
return Starship.objects.get(pk=pk)
except Starship.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
vessel = self.get_object(pk)
serializer = StarshipDetailSerialiser(vessel, context={'request': request})
return Response(serializer.data)
The detail view's URL schema is currently invoking the view based on the UUID:
...
url(r'vessels/id/(?P<pk>[0-9A-Fa-f\-]+)/$', StarshipDetail.as_view(), name='starship-detail'),
...
I now want users to be able to navigate and find the same vessel not just by UUID but also by their hull number, so that e.g. vessels/id/abcde1345...and so on.../ and vessels/hull/H1025/ would be able to resolve to the same entity. And ideally, regardless of whether one arrived at the detail view from ID or hull number, the serialiser, which is used with slight alterations in lists as well, should be able to have the ID hyperlinked to the ID-based link and the hull hyperlinked to a hull number based link (vessels/hull/H1025/). Is this at all possible? And if so, how would I go about it?
1. Add the new routes
# in urls.py
urlpatterns = [
...,
url(r'vessels/id/(?P<pk>[0-9A-Fa-f\-]+)/$', StarshipDetail.as_view(), name='starship-detail-pk'),
url(r'vessels/hull/(?P<hull_no>[0-9A-Za-z]+)/$', StarshipDetail.as_view(), name='starship-detail-hull'),
]
Tweak the regex for hull_no as you want. Note that I gave distinct names to each route, starship-detail-pk and starship-detail-hull.
2. Add the hull field in the serializer
# in serializers.py
class StarshipListSerialiser(HyperlinkedModelSerializer):
uri = UUIDHyperlinkedIdentityField(view_name='starship-detail-pk', format='html')
hull_no = UUIDHyperlinkedIdentityField(view_name='starship-detail-hull', format='html', lookup_field='hull_no')
class Meta:
model = Starship
fields = ('uri', 'name', 'hull_no')
3. Modify the view so it can also resolve objects based on hull
# in serializers.py
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView, Response
from starwars.serializers import StarshipDetailSerialiser
from starwars.models import Starship
class StarshipDetail(APIView):
def get(self, request, pk=None, hull_no=None, format=None):
lookup = {'hull_no': hull_no} if pk is None else {'pk': pk}
vessel = get_object_or_404(Starship, **lookup)
serializer = StarshipDetailSerialiser(vessel, context={'request': request})
return Response(serializer.data)
That should be enough to get you going with the detail view:
As a final note, you should be aware that it's not RESTful for the same resource to be available at two different URLs like this. Perhaps, as an alternate design decision, you might like to consider just defining the "one true route" for a resource, and adding in a "convenience" redirect from the other locator to the canonical URL.

Django REST - How to get an object with connected user

I'm trying to create a route in Django REST Framework so that I can access a comment from the object it is related to.
My models are Comment, User and Marker, and each marker can have one comment per user.
What I would like is a way to do GET /comments/marker/{marker-pk}/ that would return the comment that the connected user left on that marker, if any.
Right now I have GET /comments/{comment-pk}/ which is the default, and if I use a #detail_route decorator on a custom method I'll only have access to comments but not by marker.
My viewset:
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = MarkerCommentSerializer
So I figured out how to do this while I was writing the question. Not sure if it is very idiomatic or RESTful though...
I added a new route:
router.register(r'comments/marker', maps_views.CommentByMarkerViewSet, base_name="comments/marker")
And a new ViewSet:
class CommentByMarkerViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
def retrieve(self, request, pk=None):
comment = get_object_or_404(Comment, user=request.user, marker__pk=pk)
serializer = self.get_serializer(comment)
return Response(serializer.data)
Now I can access the comment at /comments/marker/{marker-pk}/.

Categories

Resources