Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!',
            '[email protected]',
            ['[email protected]'],
            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 [email protected], 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.

like image 614
aalberti333 Avatar asked Jul 10 '18 20:07

aalberti333


2 Answers

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,
            '[email protected]',
            [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)

like image 83
Alexandr Tatarinov Avatar answered Oct 06 '22 18:10

Alexandr Tatarinov


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!',
            '[email protected]',
            recipient,
            fail_silently=False,
        )
        return Response("mail sent successfully")


Since recipient expects an array, so the POST payload will be like,

{
    "recipient": ["[email protected]", "[email protected]", "[email protected]"]
}
like image 26
JPG Avatar answered Oct 06 '22 18:10

JPG