I am designing a REST API for an application that has both companies and departments. Some number of users can be members of a company. This leads to the following API structure:
/companies/ - Can GET, POST.
/companies/<pk>/ - Can GET, POST, PUTCH, PATCH, DELETE.
/companies/<pk>/membership/ - Can GET (gives all users that are members of a company), POST.
/companies/<pk>/membership/<pk>/ - Can DELETE.
I've managed to implement the first 3 of the endpoints, but am having trouble with implementing the last one -- how do I implement an endpoint that has multiple <pk> values in the URL? Here's what I have so far:
Currently have a urls.py file in the api app that looks as follows:
...
url(r'^company', include(company_urls.company_router.urls,
namespace="company")),
...
urls.py in the company application.
from rest_framework import routers
from .views import CompanyViewSet
company_router = routers.DefaultRouter()
company_router.register(r'^', CompanyViewSet)
serializers.py file:
from rest_framework import serializers
from .models import Company, CompanyMembership
from My_App.users.models import Profile
class CompanySerializer(serializers.ModelSerializer):
class Meta:
model = Company
fields = ('pk', 'name', 'departments', 'members')
read_only_fields = ('pk', 'departments', 'members')
class CompanyMembershipSerializer(serializers.Serializer):
user = serializers.PrimaryKeyRelatedField(queryset=Profile.objects.all())
def create(self, validated_data):
pass
def delete(self, instance, validated_data):
pass
And the views.py file:
from .models import Company, CompanyMembership
from .serializers import CompanySerializer, CompanyMembershipSerializer
from My_Appc.users.models import Profile
class CompanyViewSet(viewsets.ModelViewSet):
queryset = Company.objects.all()
serializer_class = CompanySerializer
#decorators.detail_route(methods=['get', 'post', 'delete'])
def membership(self, request, pk):
company = self.get_object()
if request.method == 'GET':
serializer = CompanyMembershipSerializer(company)
elif request.method == 'POST':
serializer = CompanyMembershipSerializer(data=request.data)
if serializer.is_valid():
try:
user = Profile.objects.get(pk=request.data.get('user'))
user_company_membership = CompanyMembership(user=user,
company=company)
user_company_membership.save()
return Response({'status': 'User added to Company.'},
status=status.HTTP_201_CREATED)
except IntegrityError:
result = {
'status': 'Failed to add user to Company.',
'reason': 'User already part of Company.'
}
status=settings.ADDITIONAL_HTTP_STATUS_CODES[
'422_UNPROCESSABLE_ENTITY']
return Response(result, status)
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
Use different names for parameters in you url:
/companies/<company_pk>/membership/<membership_pk>/
And in you ViewSet add lookup_field and lookup_url_kwarg to point to company pk field/parameter:
class CompanyViewSet(viewsets.ModelViewSet):
lookup_field = 'pk'
lookup_url_kwarg = 'company_pk'
get_object method uses this two lookups to filter queryset so you will get company based on first pk in url.
In your membership method and custom logic to manage membership object, you can access membership pk with:
membership_pk = self.kwargs.get('membership_pk', None)
Related
Using class Based (APIView) in Django rest framework for Getting and Patch (Updating) UserInfo data.
views.py
class getUserInfo(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, format=None):
user = request.user
userinfos = user.userinfo_set.all()
serializer = UserInfoSerializers(userinfos, many=True)
return Response(serializer.data)
def patch(self, request, pk, format=None):
user = UserInfo.objects.get(id=pk)
serializer = UserInfoSerializers(instance=user, data=request.data, partial=True)
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)
serializers.py
from django.contrib.auth.models import User
from .models import UserInfo
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'first_name', 'username')
class UserInfoSerializers(serializers.ModelSerializer):
user = UserSerializer(many=False, required=True)
class Meta:
model = UserInfo
fields = ('id', 'picture', 'profession', 'user')
Everything is working so far so good. Able to GET and PATCH (Update) logged-in user data.
While Testing the API in Postman, I found out that if User1 is logged in he can change the data of User2 by only using the pk of User2.
urls.py
urlpatterns = [
path('userinfo/', views.getUserInfo.as_view(), name="UserInfo"),
path('userinfo/<str:pk>/', views.getUserInfo.as_view()),
path('api/token/', views.MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('register/', views.RegisterView.as_view(), name='auth_register'),
]
Using rest_framework_simplejwt for Auth
models.py
from django.contrib.auth.models import User
class UserInfo(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
picture = models.ImageField(upload_to="profile_pics", null=True)
profession = models.CharField(max_length=200, null=True)
def __str__(self):
return "%s's Profile Picture" % self.user
Any help would be appreciated
Don't use the primary key to get the user.You are using user = request.user to get the user on get method, use the same mechanism also on update. Then the login user can only update his/her info not others info or another way you can check the user = UserInfo.objects.get(id=pk) is same as the current user request.user . If not you can show an exception.
For Retrieving and Updating an object, you can use RetrieveUpdateAPIView
class GetUserInfo(generics.RetrieveUpdateAPIView):
permission_classes = [IsAuthenticated]
queryset = UserInfo.objects.all()
serializer_class = UserInfoSerializers
def get_object(self):
return self.request.user
Here we are getting an object, it will be called from get_object method. Instead of getting user using PK, we get the current user.
You can use same url for getting and updating the user, just change the method in postman while you hit the api. GET for retrieving and PATCH for partial update.
path('userinfo/', views.GetUserInfo.as_view(), name="UserInfo"),
I want to have an API where a user can update his own listings. Currently any authenticated user can update any listing which I found using Postman. I want to validate the user so that the API returns an error as a response if the user is not trying to update his own listing. Here is my code:
# serializers.py
class ListingSerializer(serializers.ModelSerializer):
class Meta:
model = Listing
fields = '__all__'
# api.py
class ListingViewSet(ModelViewSet):
permission_classes = [IsAuthenticatedOrReadOnly]
serializer_class = ListingSerializer
def get_queryset(self):
return Listing.objects.all()
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
# urls.py
router = routers.DefaultRouter()
router.register('api/listings', ListingViewSet, 'listings')
urlpatterns = router.urls
You just need to overwrite the perform_update function:
def perform_update(self, serializer):
obj = self.get_object()
if self.request.user != obj.created_by: # Or how ever you validate
raise PermissionDenied('User is not allowed to modify listing')
serializer.save()
You will need:
from django.core.exceptions import PermissionDenied
You can limit the object for all method change the main queryset. In this case if an inapropiate user try to access an invalid object the api return 404.
def get_queryset(self):
return Listing.objects.filter(owner=self.request.user)
As stated in DRF documentation http://www.django-rest-framework.org/api-guide/parsers/#multipartparser, in order to parse multipart/form-data, the MultiPart and form parser must be used. I have a supiscion this is a problem in the Django Rest Framework because I saw a solution on their github issue saying it work using APIView.
from django.contrib.auth.models import User
from rest_framework import viewsets
from api.serializers import UserSerializer,
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.response import Response
from rest_framework import status
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all()
parser_classes = (MultiPartParser, FormParser)
serializer_class = UserSerializer
Picture of me sending request to Postman with result:
Edit: Adding UserSerializer class
class UserSerializer(serializers.HyperlinkedModelSerializer):
snapcapsules = SnapCapsuleSerializer(
many=True,
read_only=True,
allow_null=True,
)
class Meta:
model = User
fields = ('snapcapsules', 'url', 'username', 'email', 'password', )
write_only_fields = ('password',)
def create(self, validated_data):
user = User.objects.create(
username=validated_data['username'],
email=validated_data['email'],
)
user.set_password(validated_data['password'])
user.save()
return user
def update(self, instance, validated_data):
capsules_data = validated_data.pop('snapcapsules')
for capsule in capsules_data:
SnapCapsule.objects.create(user=instance, **capsule)
return instance
This is might not the issue with the ContentType. The error response says that,the UserSerializer excpect a payloadordata which include username and password field
So, Please try to add those fields to the request body and try again
UPDATE
The problem with the Orginal Post was, he added extra headers in POSTMAN (see the pic below) .
Postman will add appropriate Headers aromatically for you. So, you don't have to explicitly mention it
I removed one line and then it worked!
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
Also, the serializer can be rewrite as follows:
class UserSerializer(serializers.HyperlinkedModelSerializer):
...
def create(self, validated_data):
return User.objects.create_user(
username=validated_data['username'],
email=validated_data['email'],
password=validated_data['password']
)
...
So I have an model serializer which consists of
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'name', 'description')
This is my ViewSet
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
This is my URLs.py file:
from django.conf.urls import include, url
from rest_framework import routers
import views
router = DefaultRouter()
router.register('user', views.UserViewSet)
urlpatterns = [
url(r'^', include(router.urls)),
url(r'^login/', include('rest_framework.urls', namespace='rest_framework'))
]
Using the serializer, I can make it print out the objects inside my database. If I have the object PK/ID, I want to be able to update the field id or name of the object. Is there a way I can do that with a patch/post request using the serializer? I'm new to this so I'd love it if someone can help me out with this.
I'm thinking of just doing a POST request, then have it do this:
user = User.objects.get(id=id)
user.name = "XXXXX"
user.save()
But I want to do this using the serializer, using a PATCH request.
the below code will help to you,
**filename : views.py**
from user.models import User
from users.serializers import UserSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
class UserList(APIView):
"""
List all users, or create a new user.
"""
def get(self, request, format=None):
users = User.objects.all()
serializer = UserSerializer(users, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = UserSerializer(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 UserDetail(APIView):
"""
Retrieve, update or delete a user instance.
"""
def get_object(self, pk):
try:
return User.objects.get(pk=pk)
except User.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
user = self.get_object(pk)
serializer = UserSerializer(user)
return Response(serializer.data)
def put(self, request, pk, format=None):
user = self.get_object(pk)
serializer = UserSerializer(user, 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):
user = self.get_object(pk)
user.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
**filename : urls.py**
from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from users import views
urlpatterns = [
url(r'^users/$', views.UserList.as_view()),
url(r'^user/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
]
urlpatterns = format_suffix_patterns(urlpatterns)
Reference : http://www.django-rest-framework.org/tutorial/3-class-based-views/
Django rest framework comes with some pre-defined concrete generic views such as UpdateAPIView, RetrieveUpdateAPIView.
First you need to create a view for user which uses one of the views which can update. The update views provides handlers for patch method on the view.
RetrieveUpdateAPIView
Used for read or update endpoints to represent a single model instance.
Provides get, put and patch method handlers.
Now, use this to create a view:
class UserDetail(generics.RetrieveUpdateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
To access this view you need to have a url which uses user's primary key to access the user:
url(r'users/(?P<pk>\d+)/$', UserDetail.as_view(), name='api-user-detail'),
Then using PATCH call you can update the user's name.
Since you're using a ModelViewset, this capability should be built in. If you use the browsable API to navigate to /user/<pk>/, you'll see the operations you can perform on that object. By default, a ModelViewset provides list(), retrieve(), create(), update(), and destroy() capability.
http://www.django-rest-framework.org/api-guide/viewsets/#modelviewset
You can also override any or all of the provided methods, however an update of a single object is built in to DRF ModelViewsets. Use curl to try a PATCH to /user/<pk>/ with the information you'd like to update.
I need to send put request to API for which I know only the foreign key.
How am supposed to do this.
models.py
class Company(models.Model):
name = models.CharField(max_length = 100)
user = models.OneToOneField(settings.AUTH_USER_MODEL, unique = True)
serializer.py
class CompanySerializer(serializers.ModelSerializer):
class Meta:
model = Company
fields = ('id', 'name','user')
views.py
class Company(object):
permission_classes = (IsAuthenticated,IsUserThenReadPatch)
serializer_class = CompanySerializer
def get_queryset(self):
user = self.request.user
return Company.objects.filter(user = user)
class CompanyDetails(Company, RetrieveUpdateAPIView, APIView):
pass
urls.py
url(r'^company/$',views.CompanyDetails.as_view()),
In order to enable all CRUD operations in DRF you probably want to use ViewSet instead of View:
# views.py
class CompanyViewSet(ViewSet):
permission_classes = (IsAuthenticated,IsUserThenReadPatch)
serializer_class = CompanySerializer
def get_queryset(self):
user = self.request.user
return Company.objects.filter(user = user)
# urls.py
router = routers.SimpleRouter()
router.register(r'company', CompanyViewSet)
urlpatterns = router.urls
The above will allow you do send all CRUD REST requests:
GET /company - list all companies
POST /company - create a company
GET /company/:id - get a single company
PUT/POST /company/:id - update a company
PATCH /company/:id - partially update a company
DELETE /company/:id - delete a company
You can read more in the DRF docs - viewsets and routers