Django rest framework, perform update doesn't work - python

I have a small messaging API where the message contains a mark read boolean field.
I'm trying to automatically update the message instance so if the user logged in after the message was created, it'll be marked as read.
class MessagesViewSet(ModelViewSet):
"""
A simple ViewSet for viewing and editing the messages
associated with the user.
"""
authentication_classes = [TokenAuthentication, ]
permission_classes = [IsAuthenticated]
serializer_class = MessageSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = FILTERS.FILTER_SET
search_fields = FILTERS.SEARCH_FIELDS
ordering_fields = FILTERS.ORDERING_FIELDS
ordering = [MessageFields.DATE, ]
def get_user(self):
user = self.request.user
return user
def get_queryset(self):
return Message.objects.filter(sent_to=self.get_user())
def perform_create(self, serializer):
"""
Set the sender to the logged in user.
"""
serializer.save(sender=self.get_user())
def perform_update(self, serializer):
"""
Update the message read field to true if necessary.
"""
date = self.kwargs[MessageFields.DATE]
mark_read = self.kwargs[MessageFields.MARK_READ]
last_login = self.get_user().last_login
# If the message hasn't been read yet.
if not mark_read:
if last_login > date:
serializer.save(mark_read=True)
pass
pass
But this is not updating the object when I access it.

The perform_update method will be ran if you send a PUT or PATCH request. What you want to do is to mark messages as True whenever user gets the messages. So you can either override get_queryset or list and retrieve functions.
For example you can try this:
class MessagesViewSet(ModelViewSet):
"""
A simple ViewSet for viewing and editing the messages
associated with the user.
"""
authentication_classes = [TokenAuthentication, ]
permission_classes = [IsAuthenticated]
serializer_class = MessageSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = FILTERS.FILTER_SET
search_fields = FILTERS.SEARCH_FIELDS
ordering_fields = FILTERS.ORDERING_FIELDS
ordering = [MessageFields.DATE, ]
def get_user(self):
user = self.request.user
return user
def get_queryset(self):
return Message.objects.filter(sent_to=self.get_user())
def list(self, request):
serializer = MessageSerializer(self.get_queryset(), many=True)
for instance in serializer.data:
instance['mark_read'] = True
serializer.save()
return Response(serializer.data)
And for routing:
urlpatterns += [path('messages/',
MessagesViewSet.as_view({'get': 'list', 'post': 'create'}))]
Also you don't need to override perform_create method, it'll work fine.

I've also come around this issue and what helped me was overriding the update method itself and getting the instance object from there...
For example in your case, add:
def update(self,request,*args,**kwargs):
instance = self.get_object()
instance.sender = self.get_user()
serializer = self.get_serializer(instance,data = request.data)
self.perform_update(serializer)
return Response(serializer.data)

Related

How can I pass an id as an argument to delete or patch an user?

I'm trying to set endpoints to PATCH or DELETE users according to a permission but my current code only allows me to apply those changes to the account I'm logged to. Is there a way for me to pass an id or an email as an argument in the body of a request to modify other accounts without using the ID in the URL? Thank you.
serializers.py
class UserModifySerializer(serializers.ModelSerializer):
class Meta:
model = User
exclude = ['id', 'user_role']
class UserSerializer(serializers.ModelSerializer):
organization = OrganizationSerializer(read_only=True)
class Meta:
model = User
fields = ['id', 'email', 'first_name', 'last_name', 'organization', 'user_role', 'language']
models.py
class UserViewSet(mixins.UpdateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
swagger_schema = None
serializer_class = UserSerializer
#action(detail=False, methods=['patch'], permission_classes = [IsAuthenticated], url_path='modify-user')
def update_user(self, request):
user = User.objects.get(id=self.request.user.id)
if user.user_role in [Roles.ORGANIZATION_USER, Roles.ORGANIZATION_ADMIN, Roles.WEBSITE_ADMIN]:
serializer = UserModifySerializer(user, data=request.data, partial=True)
if serializer.is_valid():
serializer.save(**serializer.validated_data)
return Response(status=status.HTTP_200_OK)
return Response(status=status.HTTP_401_UNAUTHORIZED)
#action(detail=False, methods=['delete'], permission_classes = [IsWebsiteAdmin, IsOrganizationAdmin])
def delete_member(self, request):
user = User.objects.get(id=self.request.user.id)
if user.user_role in [Roles.ORGANIZATION_ADMIN, Roles.WEBSITE_ADMIN]:
members = User.objects.filter(organization=self.request.user.organization)\
.filter(id=id).delete()
return Response(status=status.HTTP_200_OK)
return Response(status=status.HTTP_401_UNAUTHORIZED)
You can put the necessary data in a json format inside request's body and then access it in the view like so:
data = json.loads(request.body)
How you put the data depends on how you call the view.

How do I override viewsets.ModelViewSet in Django REST Framework?

So my model is simple as
class Face(models.Model):
uid = models.CharField(max_length=510, primary_key=True)
photo = models.ImageField(upload_to='face_photos')
serializer
class FaceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Face
fields = ['uid', 'photo']
extra_kwargs = {'uid': {'required': True}, 'photo': {'required': True}}
and view should be something like
class FaceViewSet(viewsets.ModelViewSet):
queryset = Face.objects.all()
serializer_class = FaceSerializer
permission_classes = [permissions.AllowAny]
And it works. However:
I don't want list, update, delete options. Only POST and GET.
I want to add my logic on post, so if uid exists then update, else create... as well other processing.
I want custom response after the POST.
How do I achieve this all not loosing all the goodies that viewsets.ModelViewSet provides, like validations, auto generated HTML fields in Rest API web view, etc?
This worked for me:
class FaceViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, GenericViewSet):
"""
API endpoint for adding and processing new client (by uid) face
"""
queryset = Face.objects.all()
serializer_class = FaceSerializer
permission_classes = [permissions.AllowAny]
def create(self, request):
if "uid" in request.POST:
try:
instance = Face.objects.get(pk=request.POST['uid'])
serializer = FaceSerializer(
instance=instance,
data=request.data
)
except Face.DoesNotExist:
serializer = FaceSerializer(data=request.data)
else:
serializer = FaceSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = FaceSerializer(instance=instance)
return Response(serializer.data)
for number one you can use this:
http_method_names = ['get','post']
buth get method also cintains list method

Creating custom Search filter in Django API view

I have my custom API view and I want to use Search filter in this view, but generic filter dodn't work so i want to create custom one but It doesn't work and I dont know where is problem.
view
class TaskIndexAPIView(APIView):
filter_backends = (CustomSearchFilter,)
search_fields = ('name', 'description', 'user__username')
def get_queryset(self):
return Task.objects.all()
def get(self, request):
tasks = self.get_queryset()
for i in tasks:
if i.date <= date.today():
i.delayed = 'This task is delayed'
i.save()
else:
i.delayed = ''
i.save()
serializer = IndexSerializer(tasks, many=True)
return Response(serializer.data)
My custom search filter
search_filter
class CustomSearchFilter(filters.SearchFilter):
def get_search_fields(self, view, request):
if request.get_queryset.get('name', 'user'):
return ['name', 'user']
return super(CustomSearchFilter, self).get_search_fields(view, request)
In your context, the difference between APIView and generic view is, the generic view has a method called filter_queryset() which handles the filtering and searching operations.
So here in your view, we need to include the same.
class TaskIndexAPIView(APIView):
filter_backends = (CustomSearchFilter,)
search_fields = ('name', 'description', 'user__username')
def filter_queryset(self, queryset):
"""
Given a queryset, filter it with whichever filter backend is in use.
You are unlikely to want to override this method, although you may need
to call it either from a list view, or from a custom `get_object`
method if you want to apply the configured filtering backend to the
default queryset.
"""
for backend in list(self.filter_backends):
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
def get_queryset(self):
return Task.objects.all()
def get(self, request):
the_filtered_qs = self.filter_queryset(self.get_queryset())
tasks = the_filtered_qs
for i in tasks:
if i.date &lt= date.today():
i.delayed = 'This task is delayed'
i.save()
else:
i.delayed = ''
i.save()
serializer = IndexSerializer(tasks, many=True)
return Response(serializer.data)
you will get the filtered queryset as
the_filtered_qs = self.filter_queryset(self.get_queryset())

Django Rest Framework allow not authenticated access to a view in certain cases

I have a ViewSet which permission_classes is set to (permissions.IsAuthenticated,), but I want this view to allow not authenticated access when the method is retrieve().
This is my ViewSet:
class AlbumViewSet(viewsets.ModelViewSet):
permission_classes = (permissions.IsAuthenticated,)
queryset = proxies.AlbumProxy.objects.all()
serializer_class = serializers.AlbumSerializer
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter,)
search_fields = ('name', 'description', 'company__name')
filter_fields = ('code', 'company')
def retrieve(self, request, pk):
password = request.query_params.get('password', None)
instance = proxies.AlbumProxy.objects.get(code=pk)
if instance.access_code != password and password != settings.MASTER_KEY:
raise Exception(_("Invalid password for album {}".format(instance.code)))
instance_to_return = serializers.AlbumSerializer(instance=instance, context={'request': request}).data
instance_to_return.pop('access_code')
return Response(instance_to_return)
Is there a way I can disable permission_classes when the method retrieve() is on, but to leave it working in any other case?
You can override get_permissions like so:
def get_permissions(self):
if self.action == 'retrieve':
return [] # This method should return iterable of permissions
return super().get_permissions()
Django Rest Framework gives what you need out of the box. See IsAuthenticatedOrReadOnly permission.

Change serializers on per-object basis within one ViewSet?

I'm working on a project with some social features and need to make it so that a User can see all details of his profile, but only public parts of others' profiles.
Is there a way to do this within one ViewSet?
Here's a sample of my model:
class Profile(TimestampedModel):
user = models.OneToOneField(User)
nickname = models.CharField(max_length=255)
sex = models.CharField(
max_length=1, default='M',
choices=(('M', 'Male'), ('F', 'Female')))
birthday = models.DateField(blank=True, null=True)
For this model, I'd like the birthday, for example, to stay private.
In the actual model there's about a dozen such fields.
My serializers:
class FullProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
class BasicProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = read_only_fields = ('nickname', 'sex', 'birthday')
A custom permission I wrote:
class ProfilePermission(permissions.BasePermission):
"""
Handles permissions for users. The basic rules are
- owner and staff may do anything
- others can only GET
"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
else:
return request.user == obj.user or request.user.is_staff
And my viewset:
class RUViewSet(
mixins.RetrieveModelMixin, mixins.UpdateModelMixin,
mixins.ListModelMixin, viewsets.GenericViewSet):
"""ViewSet with update/retrieve powers."""
class ProfileViewSet(RUViewSet):
model = Profile
queryset = Profile.objects.all()
permission_classes = (IsAuthenticated, ProfilePermission)
def get_serializer_class(self):
user = self.request.user
if user.is_staff:
return FullProfileSerializer
return BasicProfileSerializer
What I'd like is for request.user's own profile in the queryset to be serialized using FullProfileSerializer, but the rest using BasicProfileSerializer.
Is this at all possible using DRF's API?
We can override the retrieve() and list methods in our ProfileViewSet to return different serialized data depending on the user being viewed.
In the list method, we serialize all the user instances excluding the current user with the serializer returned from get_serializer_class() method. Then we serialize the current user profile information using the FullProfileSerializer explicitly and add this serialized data to the data returned before.
In the retrieve method, we set a accessed_profile attribute on the view to know about the user the view is displaying. Then, we will use this attribute to decide the serializer in the get_serializer_class() method.
class ProfileViewSet(RUViewSet):
model = Profile
queryset = Profile.objects.all()
permission_classes = (IsAuthenticated, ProfilePermission)
def list(self, request, *args, **kwargs):
instance = self.filter_queryset(self.get_queryset()).exclude(user=self.request.user)
page = self.paginate_queryset(instance)
if page is not None:
serializer = self.get_pagination_serializer(page)
else:
serializer = self.get_serializer(instance, many=True)
other_profiles_data = serializer.data # serialized profiles data for users other than current user
current_user_profile = <get_the_current_user_profile_object>
current_user_profile_data = FullProfileSerializer(current_user_profile).data
all_profiles_data = other_profiles_data.append(current_user_profile_data)
return Response(all_profiles_data)
def retrieve(self, request, *args, **kwargs):
self.accessed_profile = self.get_object() # set this as on attribute on the view
serializer = self.get_serializer(self.accessed_profile)
return Response(serializer.data)
def get_serializer_class(self):
current_user = self.request.user
if current_user.is_staff or (self.action=='retrieve' and self.accessed_profile.user==current_user):
return FullProfileSerializer
return BasicProfileSerializer
I managed to hack together the solution that provides the wanted behaviour for the detail view:
class ProfileViewSet(RUViewSet):
model = Profile
queryset = Profile.objects.all()
permission_classes = (IsAuthenticated, ProfilePermission)
def get_serializer_class(self):
user = self.request.user
if user.is_staff:
return FullProfileSerializer
return BasicProfileSerializer
def get_serializer(self, instance=None, *args, **kwargs):
if hasattr(instance, 'user'):
user = self.request.user
if instance.user == user or user.is_staff:
kwargs['instance'] = instance
kwargs['context'] = self.get_serializer_context()
return FullProfileSerializer(*args, **kwargs)
return super(ProfileViewSet, self).get_serializer(
instance, *args, **kwargs)
This doesn't work for the list view, however, as that one provides the get_serializer method with a Django Queryset object in place of an actual instance.
I'd still like to see this behaviour in a list view, i.e. when serializing many objects, so if anyone knows a more elegant way to do this that also covers the list view I'd much appreciate your answer.

Categories

Resources