Use custom function in ModelViewSet with Django Rest Framework - python

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"]
}

Related

Django rest api create email login along with password

I've created the employee management system using django rest api. I've created the models, views and serializers like shown below. What i need is I've created employee details like his personal details as a register view, but when he login i want to use email and password field as a field to sign with jwt token. How to implement this.
Models.py:
Class Employee (models.Model):
Fname = models.charfield()
Lname = models.charfield()
Personal _email=models.Emailfield()
Department= models.choicefield()
Designation= models.charfield()
Serializer.py:
Class Employeeserializer(models.Modelserializer):
class Meta:
model = Employee
fields = '__all__
Views.py:
Registeremployeeview(createAPIview):
serializer_class = Employeecreateserializer
def post(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response({
"user": serializer.data,
"message": "User Created Successfully..!",
})
Now i want to create login view. How to i make email and password login view and serializers without I haven't included that in my models.
Alos i want to login and set authentication with jwt authentication.
Can you tell me why one or more of the pre-existing authentication mechanisms as described here wouldn't work?
https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#tutorial-4-authentication-permissions
Django rest framework supports many out of the box authentication mechanisms, and you can even use custom ones, but you really need to do a bit of reading first to understand the intricacies of each mechanism before deciding which to use, or whether to use a custom one.

How can I set permission for seperate request methods in DRF ModelViewSet?

I'm fairly new to Django and Django Rest Framework and I can't figure out why my code isn't working.
I have a Biz model that has a few fields:
class Biz(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
title = models.CharField(max_length=200)
description = models.TextField()
address = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=100)
phone = PhoneNumberField()
which I serializer using ModelSerializer:
class BizSerializer(serializers.ModelSerializer):
class Meta:
model = Biz
fields = "__all__"
And I use ModelViewSet to have an endpoint for it:
class BizViewSet(viewsets.ModelViewSet):
queryset = Biz.objects.all()
authentication_classes = (authentication.TokenAuthentication,)
permission_classes = [HasGroupPermission]
required_groups = {
"GET": ["__all__"],
"POST": ["member", "biz_post"],
"PUT": ["member", "biz_edit"],
"PATCH": ["member", "biz_edit"],
}
serializer_class = BizSerializer
You probably noticed HasGroupPermission. It is a custom permission I made to confirm the requesting user is in required group(s) the code is:
def is_in_group(user, group_name):
"""
Takes a user and a group name, and returns `True` if the user is in that group.
"""
try:
return Group.objects.get(name=group_name).user_set.filter(id=user.id).exists()
except Group.DoesNotExist:
return None
class HasGroupPermission(permissions.BasePermission):
"""
Ensure user is in required groups.
"""
def has_permission(self, request, view):
# Get a mapping of methods -> required group.
required_groups_mapping = getattr(view, "required_groups", {})
# Determine the required groups for this particular request method.
required_groups = required_groups_mapping.get(request.method, [])
# Return True if the user has all the required groups or is staff.
return all(
[
is_in_group(request.user, group_name)
if group_name != "__all__"
else True
for group_name in required_groups
]
) or (request.user and request.user.is_staff)
However, when I make a GET request, the permission function works like it's supposed to and allows everyone to make the request, and when i make a POST request, the permission function also works perfectly (if user isn't in both "member" and "biz_post" groups the request is denied).
The problem arises when I try other methods such as PUT, PATCH, and DELETE. Why is this issue happening? Half the methods work and the other half (sorta) don't. My knowledge in DRF is limited at the moment, and I can't seem to solve the issue.
I realized my problem which I found very silly. My BizViewSet is actually a ViewSet and I didn't realize that I have to make PATCH, PUT, and DELETE requests to the object link (as in localhost:8000/api/biz/$id). Since my User serializer isn't a ViewSet I thought the patch method works the same way which was I pass a primary key in JSON along with the data I wanted to patch but ViewSets are different and I didn't know that. Silly.
Hi you can use DjangoModelPermissions instead of HasGroupPermission
(at the first you must import it)
from rest_framework.permissions import DjangoModelPermissions
This permission check that user have permission for PUT, POST and DELETE
All user have GET permission
You must set permission for user in admin or set permission for group of user
I hope it helps you
has_permission method not provide object-level permission and PUT and PATCH need object-level permission.
You must create object-level permissions, that are only run against operations that affect a particular object instance by using has_object_permission method of permissions.BasePermission class.
See this link.
Hope it helped.

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

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

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.

How can i make django-rest-framework-jwt return token on registration?

I have a basic django rest service, which
registers a person and
updates his password.
I want to add jwt authentication on top of it. If I follow the tutorial I would need to add a new url named "api-token-auth" in project's urls.py. But, I don't want to add this new url and want my register call to send a token in response.
Here's my code:
serializers.py
class UserSerializer(serializers.HyperlinkedModelSerializer):
def create(self, validated_data):
user = User(
username=validated_data['username']
)
user.set_password(validated_data['password'])
user.save()
return user
def update(self, instance, validated_data):
instance.set_password(validated_data['password'])
instance.save()
return instance
class Meta:
model = User
fields = ('url', 'username', 'password')
lookup_field = 'username'
write_only_fields = ('password',)
views.py
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.exclude(is_superuser=1)
serializer_class = UserSerializer
lookup_field = 'username'
What should be done to achieve this ? should I call api-auth-token inside my serializer's create method ?
How does django-rest-framework-jwt handles multiple authentication tokens and correctly identifies which token belongs to which user ? Especially when it doesn't store tokens in a db.
How can I use this authentication mechanism to limit my user to view/update/delete only his user ?
How can I use this authentication mechanism to do anything in general. For instance if a user wants to write his name to /tmp/abcd.txt. How can I make sure that only authenticated users would be able to do so ?
Are there any potential loopholes in this approach. Should I use the same code if my app is going to store lots of classified data ?
Question 1: To generate tokens manually on registration you can define and make use of a method like this:
import jwt
from rest_framework_jwt.utils import jwt_payload_handler
def create_token(user):
payload = jwt_payload_handler(user)
token = jwt.encode(payload, settings.SECRET_KEY)
return token.decode('unicode_escape')
you can add this method to the view and generate the token once the user has been registered and return it in the response.
Question 2: JWT tokens do not need to be stored in the database. You can read more about how JWT works at http://jwt.io/.
Question 3 and 4: To use tokens to limit access to a specific view, especially an APIView or one of its subclasses or a view provided by Django Rest framework, you need to specify the permission classes. for example:
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
class ExampleView(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
Question 5: One potential loophole while working with Django Rest Framework is the default permissions that you setup from the settings of your application; if for example you AllowAny in the settings it'll make all the views publicly accessible unless you specifically override the permission classes in each view.
The Accepted answer has some code that generates token but it doesn't show how to integrate it in serializer/view. Also not sure that manual jwt.encode is a good modern way of doing this if we already have jwt_encode_handler
to do this. You can create SerializerMethodField and create tokens there:
token = serializers.SerializerMethodField()
def get_token(self, obj):
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(obj)
token = jwt_encode_handler(payload)
return token
Then add token field to Meta.fields.
Working Example

Categories

Resources