// 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 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.
Most primitive and the least secure authentication system provided by DRF.
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
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.
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.
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/
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 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')
// ]
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
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
classThe 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.
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()
.
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
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.