Django Rest Framework 3/3

JunePyo Suh·2020년 7월 7일
0

Extending Django's User Model with a custom Profile Model

// models.py
from django.db import models
from django.contrib.auth.models import User


class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.CharField(max_length=240, blank=True)
    city = models.CharField(max_length=30, blank=True)
    avatar = models.ImageField(null=True, blank=True)

    def __str__(self):
        return self.user.username


class ProfileStatus(models.Model):
    user_profile = models.ForeignKey(Profile, on_delete=models.CASCADE)
    status_content = models.CharField(max_length=240)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name_plural = "statuses"

    def __str__(self):
        return str(self.user_profile)
// signals.py
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
from profiles.models import Profile


@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
    print("created: ", created)
    if created:
        Profile.objects.create(user=instance)

// apps.py
from django.apps import AppConfig


class ProfilesConfig(AppConfig):
    name = 'profiles'

    def ready(self):
        import profiles.signals

// __init__.py
default_app_config = 'profiles.apps.ProfilesConfig'

Authentication in DRF

Authentication vs Permissions

Authentication is always run at the very start of views, before the authorization checks occur, and before any other code is executed.
Authentication by itself won't allow or disallow an incoming request; it simply identifies the credentials that the request was made with.

Basic Authentication

Most primitive and the least secure authentication system provided by DRF.

The request/response cycle goes like the following:

  1. The client makes a HTTP request to the server
  2. The server responds with a HTTP 401 Unauthorized response containing the WWW-Authenticate header, explaining how to authenticate (WWW - Authenticate: Basic)
  3. The client sends its auth credentials in base 64 with the Authorization header. (Authentication credentials here are unencrypted)
  4. The server evaluates the access credentials and responds with the 200 or 403 status code, thereby authorizing or denying the client's request.

Basic Authentication is generally only appropriate for testing

Token Authentication

Saving the authentication token in localStorage is very dangerous, as it makes it vulnerable to XSS attacks!

Using a httpOnly cookie is much safer as the token won't be accessed via JavaScript, although you may lose some flexibility.

JSON web tokens can be easily used in a DRF powered REST API.

pip install django-rest-framework-simplejwt

Session Authentication

Uses Django's default session backend for authentication. It is the safest and most appropriate way of authentication AJAX clients that are running in the same session context as your website, and uses a combination of sessions and cookies.

The request/response cycle goes like the following:

  1. Users send their authentication credentials (via Login)
  2. The server checks the data and if correct, it creates a corresponding Session Object that will be saved in the database, sending back to the client a Session ID.
  3. The Session ID gets saved in a Cookie in the browser and will be part of every future request to the server, that will check it every time.
  4. When the client logs out, the Session ID is destroyed by both the client and the server, and a new one will be created at the next login.

If successfully authenticated using Session Authentication, Django will provide us the corresponding User Object, accessible via request.user.

For non-authenticated requests, an AnnonymousUser instance will be provided instead.

Important: Once authenticated via session auth, the framework will require a valid CSRF token to be sent for any unsafe HTTP method request such as PUT, PATCH, POST, DELETE.

The CSRF token is an important Cross-Site Request Forgery vulnerability protection.

Django-REST-Auth

Setting the authentication scheme

pip install django-rest-auth

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
      'rest_framework.authentication.TokenAuthentication',
      'rest_framework.authentication.SessionAuthentication',
    ]
}

// In myapp/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('profiles.api.urls')),
    path("api/rest-auth/", include("rest_auth.urls")), // used for api/rest-auth/login/ and api/rest-auth/registration/
    path("api-auth/", include("rest_framework.urls")), // used for api-auth/login
    path("api/rest-auth/registration/", include("rest_auth.registration.urls")) 
]

// If you need to handle files locally:
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,
                          document_root=settings.MEDIA_ROOT)

api/rest-auth/login/

api/rest-auth/registration/

api-auth/login/

Example client() to test auth views

import requests

def client():
    // you must adhere to this token declaration format
    token_h = "Token 84886ece00fabcdfec0ec8ea4c4ffb23b74zd6590"
    // credentials = {"username": "admin", "password": "complex123"}
    headers = {'Authorization': token_h}
    response = requests.get(
        "http://127.0.0.1:8000/api/profiles", headers=headers)
    print("Status Code: ", response.status_code)
    response_data = response.json()
    print(response_data)


if __name__ == "__main__":
    client()

Viewset and Router Classes

Viewset classes allow us to combine the logic for a set of related views in a single class: a ViewSet could for example allow us to get a list of elements from a queryset, but also allow us to get the details of a single instance of the same model.

ViewSet work at the highest abstraction level compared to all the API views that we have learned to use so far.

ViewSets are in fact another kind of Class Based View, which does not provide any method handlers such as .get() or .post(), and instead provides action methods such as .list() and .create().

ViewSets are typically used in combination with the Router class, allowing us to automatically get a url path configuration that is appropriate to the different kind of actions that the ViewSet provides.

// views.py (A new class based view using ViewSet, instead of ListAPIView)
// class ProfileList(generics.ListAPIView):
//     queryset = Profile.objects.all()
//     serializer_class = ProfileSerializer
//     permission_classes = [IsAuthenticated]

class ProfileViewSet(ReadOnlyModelViewSet):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer
    permission_classes = [IsAuthenticated]

// urls.py
profile_list = ProfileViewSet.as_view({"get": "list"})  # HTTP verb : action
profile_detail = ProfileViewSet.as_view({"get": "retrieve"})

urlpatterns = [
    path("", include(router.urls)),
]
// urlpatterns = [
//     path("profiles/", profile_list, name='profile-list'),
//     path("profiles/<int:pk>/", profile_detail, name='profile-detail')
// ]

How can we get two different endpoints from a same view class?

source code

class ViewSetMixin:
    """
    This is the magic.

    Overrides `.as_view()` so that it takes an `actions` keyword that performs
    the binding of HTTP methods to actions on the Resource.

    For example, to create a concrete view binding the 'GET' and 'POST' methods
    to the 'list' and 'create' actions...

    view = MyViewSet.as_view({'get': 'list', 'post': 'create'})
    """
	...
    
class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
    """
    The GenericViewSet class does not provide any actions by default,
    but does include the base set of generic view behavior, such as
    the `get_object` and `get_queryset` methods.
    """
    pass


class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
                           mixins.ListModelMixin,
                           GenericViewSet):
    """
    A viewset that provides default `list()` and `retrieve()` actions.
    """
    pass

Quick Recap on View & ViewSet

Views

  • Function based views
  • Class based views

APIView

class JournalistListCreateAPIView(APIView):
    def get(self, request):
        journalists = Journalist.objects.all()
        serializer = JournalistSerializer(journalists, many=True, context={'request':request})
        return Response(serializer.data)

    def post(self, request):
        serializer = JournalistSerializer(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)

GenericAPIView : typically when using the generic views, you will override the view and set several class attributes, or use concrete generic views.

class EbookListCreateAPIView(generics.ListCreateAPIView):
    queryset = Ebook.objects.all().order_by("-id")
    serializer_class = EbookSerializer
    permission_classes = [IsAdminUserOrReadOnly]
    pagination_class = SmallSetPagination

ViewSet class

The ViewSet class inherits from APIView. The ViewSet class itself does not provide any implementations, so you would have to override the class and define the actions

GenericViewSet

The GenericViewSet class inherits from GenericAPIView, and provides the default set of get_object, get_queryset methods and other generic view base behavior. To use this class override the class and required mixin classes, or define the action implementations.

ModelViewSet

The ModelViewSet class inherits from GenericAPIView and includes implementations for various actions, by mixing in the behavior of the various Mixin classes.

  • ViewSet actions
    def list(self, request), def create(self, request), def retrieve(self, request, pk=None), def update(self, request, pk=None), def partial_update(self, request, pk=None), def destroy(self, request, pk=None).

ReadOnlyModelViewSet

The ReadOnlyModelViewSet class also inherits from GenericAPIView. As with ModelViewSet it also includes implementations for various actions, but unlike ModelViewSet only provides the 'read-only' actions, .list() and .retrieve().

ViewSets are powerful, but...

Sometimes combining concrete API View with ViewSets can result in more flexible and robust API. For example, Profile model has avatar ImageField declared.

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.CharField(max_length=240, blank=True)
    city = models.CharField(max_length=30, blank=True)
    avatar = models.ImageField(null=True, blank=True)

    def __str__(self):
        return self.user.username

Instead of updating avatar along with other Profile fields in one ViewSet, create another separate endpoint with AvatarUpdateView(generics.UpdateAPIView).

class AvatarUpdateView(generics.UpdateAPIView):
    serializer_class = ProfileAvatarSerializer
    permission_classes = [IsAuthenticated]

    def get_object(self):
        profile_object = self.request.user.profile
        return profile_object

Filtering via DRF

get_queryset()

class ProfileStatusViewSet(ModelViewSet):
    serializer_class = ProfileStatusSerializer
    permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]

    def get_queryset(self):
        queryset = ProfileStatus.objects.all()
        # check for query parameter
        username = self.request.query_params.get("username", None)
        if username is not None:
            queryset = queryset.filter(user_profile__user__username=username)
        return queryset

    def perform_create(self, serializers):
        user_profile = self.request.user.profile
        serializers.save(user_profile=user_profile)

Because queryset attribute has disappeared from this ViewSet class and was relocated to def get_queryset, you need to make the following changes in urls.py.

router.register(r"status", ProfileStatusViewSet, basename='status')

According to DRf documentation,

The basename argument is used to specify the initial part of the view name pattern.
Typically you won't need to specify the basename argument, but if you have a viewset where you've defined a custom get_queryset method, then the viewset may not have a .queryset attribute set.
This is because removal of the queryset property from your ViewSet will render any associated router to be unable to derive the basename of your Model automatically.

Write Automated Tests

0개의 댓글