How to clear a ManyToMany Field in a patch request? - django

My ManyToMany Relationship doesn't reset. I'm doing a patch requests that translates into djrf's partial_update. But afterwards RecordUsersEntry still has the same users saved it got from setup_entry.
I've tried a put request with field and record, and then the many to many relationship is resetted, but I want to reset it with a patch request.
Might be related to: https://github.com/encode/django-rest-framework/issues/2883, however I'm going to use JSON Requests and at the moment I'm only concerned about how to get this test green.
I've written the follwing test:
def test_entry_update(self):
self.setup_entry()
view = RecordUsersEntryViewSet.as_view(actions={'patch': 'partial_update'})
data = {
'users': []
}
request = self.factory.patch('', data=data)
force_authenticate(request, self.user)
response = view(request, pk=1)
self.assertEqual(response.status_code, 200)
entry = RecordUsersEntry.objects.first()
self.assertEqual(entry.users.all().count(), UserProfile.objects.none().count()) # <-- The test fails here
with
def setup_entry(self):
self.entry = RecordUsersEntry.objects.create(record=self.record, field=self.field)
self.entry.users.set(UserProfile.objects.all())
and the model looks like this:
class RecordUsersEntry(RecordEntry):
record = models.ForeignKey(Record, on_delete=models.CASCADE, related_name='users_entries')
field = models.ForeignKey(RecordUsersField, related_name='entries', on_delete=models.PROTECT)
users = models.ManyToManyField(UserProfile, blank=True)
class Meta:
unique_together = ['record', 'field']
verbose_name = 'RecordUsersEntry'
verbose_name_plural = 'RecordUsersEntries'
def __str__(self):
return 'recordUsersEntry: {};'.format(self.pk)
Viewsets and Serializer just being the basic ones:
class RecordUsersEntryViewSet(mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin,
GenericViewSet):
queryset = RecordUsersEntry.objects.none()
serializer_class = RecordUsersEntrySerializer
def get_queryset(self):
# every field returned because they are supposed to be seen by everybody
return RecordUsersEntry.objects.filter(record__template__rlc=self.request.user.rlc)
Serializer:
class RecordUsersEntrySerializer(serializers.ModelSerializer):
class Meta:
model = RecordUsersEntry
fields = '__all__'

I've figured it out, I had to specify the format, because it seems like the tests are otherwise sending it in multipart/form-data which doesn't work, see: https://github.com/encode/django-rest-framework/issues/2883
Therefore, this works:
request = self.factory.patch('', data=data, format='json')

Related

REST Django - How to Modify a Serialized File Before it is Put Into Model

I am hoping that I can find a way to resize an uploaded image file before it is put into the database.
I am new to Django with REST, so I am not sure how this would be done. It seems that whatever is serialized is just kind of automatically railroaded right into the model. Which I suppose is the point (it's certainly an easy thing to setup).
To clarify, I already have a function tested and working that resizes the image for me. That can be modified as needed and is no problem for me. The issue really is about sort of "intercepting" the image, making my changes, and then putting it into the model. Could someone help me out with some ideas of tactics to get that done? Thanks.
The Model:
class Media(models.Model):
objects = None
username = models.ForeignKey(User, to_field='username',
related_name="Upload_username",
on_delete=models.DO_NOTHING)
date = models.DateTimeField(auto_now_add=True)
media = models.FileField(upload_to='albumMedia', null=True)
file_type = models.CharField(max_length=12)
MEDIA_TYPES = (
('I', "Image"),
('V', "Video")
)
media_type = models.CharField(max_length=1, choices=MEDIA_TYPES, default='I')
user_access = models.CharField(max_length=1, choices=ACCESSIBILITY, default='P')
class Meta:
verbose_name = "MediaManager"
The View with post method:
class MediaView(APIView):
queryset = Media.objects.all()
parser_classes = (MultiPartParser, FormParser)
permission_classes = [permissions.IsAuthenticated, ]
serializer_class = MediaSerializer
def post(self, request, *args, **kwargs):
user = self.request.user
print(user.username)
request.data.update({"username": user.username})
media_serializer = MediaSerializer(data=request.data)
# media_serializer.update('username', user.username)
if media_serializer .is_valid():
media_serializer.save()
return Response(media_serializer.data, status=status.HTTP_201_CREATED)
else:
print('error', media_serializer.errors)
return Response(media_serializer.errors,status=status.HTTP_400_BAD_REQUEST)
The Serializer:
class MediaSerializer(serializers.ModelSerializer):
class Meta:
model = Media
fields = '__all__'
def to_representation(self, instance):
data = super(MediaSerializer, self).to_representation(instance)
return data
You can use validate method to validate and/or change the values from data dictionary.
class MediaSerializer(serializers.ModelSerializer):
...
def validate(self, data):
value_from_form = data['value_from_form']
value_from_form = 'Something else'
data['value_from_form'] = value_from_form
return data

How to update OneToOneField using Django Rest Framework

I'm trying to make an image upload through a REST API from a mobile client. I've managed to implement it using an multipart request to the REST endpoint, but when I try to update the image, the request is not handled correctly because of the constraints on the OneToOneField.
This is how I implemented the API:
models.py
class Hotel(models.Model):
name = models.CharField(max_length=500, null=False, blank=False)
address = models.CharField(max_length=500, null=False, blank=False)
rating = models.FloatField()
owner = models.CharField(max_length=200, null=False)
class Meta:
ordering = ['name']
class HotelPhoto(models.Model):
photo = models.ImageField(upload_to='hotel_photos', null=True)
hotel = models.OneToOneField(Hotel, on_delete=models.CASCADE, primary_key=True)
views.py
class HotelPhotoUpload(APIView):
parser_classes = [FormParser, MultiPartParser]
def post(self, request):
photo_serializer = HotelPhotoSerializer(data=request.data,
context={'request': request})
if photo_serializer.is_valid():
photo_serializer.save()
return Response(photo_serializer.data,
status=status.HTTP_201_CREATED)
else:
logger.error(f'Error uploading image: {photo_serializer.errors}')
return Response(photo_serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
serializers.py
class HotelSerializer(serializers.HyperlinkedModelSerializer):
photo = serializers.ImageField(source='hotelphoto.photo', read_only=True)
class Meta:
model = Hotel
fields = ['url', 'id', 'name', 'address', 'rating', 'owner', 'photo']
class HotelPhotoSerializer(serializers.ModelSerializer):
photo_url = serializers.SerializerMethodField()
class Meta:
model = HotelPhoto
fields = ['hotel', 'photo', 'photo_url']
def get_photo_url(self, obj):
return self.context['request'].build_absolute_uri(obj.photo.url)
This is the error I'm getting:
Error uploading image: {'hotel': [ErrorDetail(string='hotel photo with this hotel already exists.', code='unique')]}
Bad Request: /hotels/photo/upload/
I understand this is due to the constraint on the OneToOneField since I've have already uploaded a photo, but how should I do the request in order to just update the HotelPhoto.photo field?
What I've tried
Implementing a put method on the HotelPhotoUpload view with partial=True on the serializer, but it gave the same error.
I thought about overwriting the validate method on the serializer, but I don't know if I need to validate anything on the photo itself. I was hoping the framework would handle this for me.
Thought about merging the HotelPhoto and Hotel models, but that would require a big refactor of other code.
EDIT:
I'm currently using django 3.0.2.
Following the answer by neferpitou, I've managed to get it working after these minor changes:
serializers.py
# Didn't change the HotelSerializer
class HotelPhotoSerializer(serializers.ModelSerializer):
hotel = serializers.PrimaryKeyRelatedField(
many=False,
queryset=Hotel.objects.all())
class Meta:
model = HotelPhoto
fields = ['hotel', 'photo']
def create(self, validated_data):
# Instead of creating a new HotelPhoto instance
# changed the photo field from the Hotel instance
hotel = validated_data.get('hotel')
photo = validated_data.get('photo')
hotel.hotelphoto.photo.save(photo.name, photo)
hotel.save()
return hotel.hotelphoto
views.py
class HotelPhotoUpload(APIView):
parser_classes = [FormParser, MultiPartParser]
def post(self, request):
# I'm already sending the hotel id on the POST request
photo_serializer = HotelPhotoSerializer(data=request.data)
if photo_serializer.is_valid():
photo_serializer.save()
return Response(photo_serializer.data,
status=status.HTTP_201_CREATED)
else:
logger.error(f'Error uploading image: {photo_serializer.errors}')
return Response(photo_serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
One thing that I forgot to mention is that I'm always sending a request (either POST or PUT) to the hotels endpoint (which uses the HotelSerializer) before uploading a photo. So I'm not expecting to have problems on the photo/upload endpoint due to an inexistent hotel.
mobile client
Unfortunaly can't post the Multipart POST request content because it's huge. But here is the client method implementation using Retrofit 2.5.0.
// Sends the hotel id and the entire content of the photo file.
#Multipart
#POST(UPLOAD_ENDPOINT)
fun uploadPhoto(#Part("hotel") hotelId: RequestBody,
#Part photo: MultipartBody.Part) : Call<UploadResult>
companion object {
const val UPLOAD_ENDPOINT = "hotels/photo/upload/"
}
Foreignkey and OneToOneField can be serialized in the same manner.
Here is your
views.py
class HotelPhotoUpload(APIView):
# parser_classes = [FormParser, MultiPartParser]
def post(self, request):
hotel = Hotel.objects.get(name=request.data.get('hotel'))
request.data['hotel'] = hotel.id
photo_serializer = HotelPhotoSerializer(data=request.data)
# print(photo_serializer)
if photo_serializer.is_valid():
photo_serializer.save()
return Response(photo_serializer.data,
status=status.HTTP_201_CREATED)
else:
# logger.error(f'Error uploading image: {photo_serializer.errors}')
return Response(photo_serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
serializers.py
class HotelSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Hotel
fields = '__all__'
class HotelPhotoSerializer(serializers.ModelSerializer):
hotel = serializers.PrimaryKeyRelatedField(many=False, queryset=Hotel.objects.all())
class Meta:
model = HotelPhoto
fields = ['hotel', 'photo',]
def create(self, validated_data):
hotel_photo = HotelPhoto.objects.create(**validated_data)
hotel_photo.save()
return hotel_photo
I am confused why there are extra fields in your HotelSerializer, so I have trimmed it down. If you have specific use case for those feel free to modify in your code. And there are no primary key in your Hotel model, so it will create id field by default and I am assuming every hotel name in unique.
Postman request:
Hotel Data From Admin Section:

Django Rest Framework unique together validation with field absent from request

I'm implementing some voting functionality in an application, where a logged-in user specifies a post that they would like to vote for using a payload like this:
{
"post": 1,
"value": 1
}
As you can tell, the a user field is absent - this is because it gets set in my viewset's perform_create method. I've done this to ensure the vote's user gets set server side. This is what the viewset looks like:
class CreateVoteView(generics.CreateAPIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = VoteSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Here is what the model looks like:
class Vote(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='votes', null=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='votes', null=False)
class Values(models.IntegerChoices):
UP = 1, _('Up')
DOWN = -1, _('Down')
value = models.IntegerField(choices=Values.choices, null=False)
class Meta:
unique_together = ('post', 'user')
and finally, the serializer:
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ['post', 'value']
From what I understand, in order for DRF to enforce a unique together validation, both fields (in my case, user and post) must be included in the serializer's fields. As I've mentioned, I'd like to avoid this. Is there any other way of implementing this type of validation logic?
EDIT:
To clarify: the records do not save - I receive this error:
django.db.utils.IntegrityError: (1062, "Duplicate entry '1-3' for key 'api_vote.api_vote_post_id_user_id_73614533_uniq'")
However, my goal is to return a Bad Request instead of an Internal Server Error much like I would when traditionally using a DRF serializer and excluding required fields from a payload.
To output a custom error message due to the IntegrityError, you can override the create method in your serializer:
from django.db import IntegrityError
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ['post', 'value']
def create(self, validated_data):
try:
validated_data['user'] = self.context['request'].user
return super().create(validated_data)
except IntegrityError:
error_msg = {'error': 'IntegrityError message'}
raise serializers.ValidationError(error_msg)
You can try this on your views
try:
MoviesWatchList.objects.create(user=request.user, content=movie)
return response.Response({'message': f'{movie} added in watchlist.'}, status=status.HTTP_201_CREATED)
except:
return response.Response({'message': f'{movie} already added to watchlist.'}, status=status.HTTP_304_NOT_MODIFIED)

create object with foreign key to User DRF

I am using the django rest framework and I have a very simple model of Posts for a particular user which I have serialised in the following manner.
Serializers.py
class PostSerializer(serializers.HyperlinkedModelSerializer):
image = serializers.ImageField(max_length=None, use_url=True)
question = serializers.CharField(required=False)
ayes = serializers.CharField(required=False)
nays = serializers.CharField(required=False)
neutrals = serializers.CharField(required=False)
class Meta:
model = Posts
fields = ('user','question', 'image','ayes', 'nays', 'neutrals')
My models.py is as follows
class Posts(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
question = models.TextField(max_length=500, blank=True)
image = models.ImageField('optionalImage', upload_to='images/posts/', default='/images/posts/blank.png')
ayes = models.TextField(max_length=200, default=0)
nays = models.TextField(max_length=200, default=0)
neutrals = models.TextField(max_length=200, default=0)
When I tried posting to this I kept getting NOT NULL Integrity constraint error of user_id. Hence I added context={'request': request}) to the serializer which ends up giving me the following error:
Could not resolve URL for hyperlinked relationship using view name "user-detail". You may have failed to include the related model in your API, or incorrectly configured the lookup_field attribute on this field.
My views.py is as follows:
views.py
#permission_classes((IsAuthenticated, ))
class PostsView(generics.ListCreateAPIView):
queryset = Posts.objects.all()
serializer_class = PostSerializer
def get(self, request, format=None):
snippets = Posts.objects.filter(pk=request.user.id)
serializer = PostSerializer(snippets, many=True,context={'request': request})
return Response(serializer.data)
def post(self, request, format=None):
posts = PostSerializer(data=request.data,context={'request': request})
if posts.is_valid():
posts.save()
return Response("YOLO", status=status.HTTP_201_CREATED)
else:
return Response(posts.errors, status=status.HTTP_400_BAD_REQUEST)
All other fields of mine have posted correctly when I set default=0 in my model for user. I am unable to submit the foreign key user which needs to be saved on every post. What am I doing wrong here? Am I following the correct method?
Since you don't want to send your user, you should remove it from the serializer's field.
Next, you want to set the post's user to the current user. To achieve that, you want to pass the request.user to the serializer's data by changing the save method to:
posts.save(user=request.user)
It's explained in the documentation and in the tutorial

Per Field Permission in Django REST Framework

I am using Django REST Framework to serialize a Django model. I have a ListCreateAPIView view to list the objects and a RetrieveUpdateDestroyAPIView view to retrieve/update/delete individual objects. The model stores information that the users submit themselves. The information they submit contains some private information and some public information. I want all users to be able to list and retrieve the public information but I want only the owner to list/retrieve/update/delete the private information. Therefore, I need per-field permissions and not object permissions.
The closest suggestion I found was https://groups.google.com/forum/#!topic/django-rest-framework/FUd27n_k3U0 which changes the serializer based on the request type. This won't work for my situation because I don't have the queryset or object at that point to determine if it is owned by the user or not.
Of course, I have my frontend hiding the private information but smart people can still snoop the API requests to get the full objects. If code is needed, I can provide it but my request applies to vanilla Django REST Framework designs.
How about switching serializer class based on user?
In documentation:
http://www.django-rest-framework.org/api-guide/generic-views/#get_serializer_classself
def get_serializer_class(self):
if self.request.user.is_staff:
return FullAccountSerializer
return BasicAccountSerializer
I had a similar problem the other day. Here is my approach:
This is a DRF 2.4 solution.
class PrivateField(serializers.Field):
def field_to_native(self, obj, field_name):
"""
Return null value if request has no access to that field
"""
if obj.created_by == self.context.get('request').user:
return super(PrivateField, self).field_to_native(obj, field_name)
return None
#Usage
class UserInfoSerializer(serializers.ModelSerializer):
private_field1 = PrivateField()
private_field2 = PrivateField()
class Meta:
model = UserInfo
And a DRF 3.x solution:
class PrivateField(serializers.ReadOnlyField):
def get_attribute(self, instance):
"""
Given the *outgoing* object instance, return the primitive value
that should be used for this field.
"""
if instance.created_by == self.context['request'].user:
return super(PrivateField, self).get_attribute(instance)
return None
This time we extend ReadOnlyField only because to_representation is not implemented in the serializers.Field class.
I figured out a way to do it. In the serializer, I have access to both the object and the user making the API request. I can therefore check if the requestor is the owner of the object and return the private information. If they are not, the serializer will return an empty string.
class UserInfoSerializer(serializers.HyperlinkedModelSerializer):
private_field1 = serializers.SerializerMethodField('get_private_field1')
class Meta:
model = UserInfo
fields = (
'id',
'public_field1',
'public_field2',
'private_field1',
)
read_only_fields = ('id')
def get_private_field1(self, obj):
# obj.created_by is the foreign key to the user model
if obj.created_by != self.context['request'].user:
return ""
else:
return obj.private_field1
Here:
-- models.py:
class Article(models.Model):
name = models.CharField(max_length=50, blank=False)
author = models.CharField(max_length=50, blank=True)
def __str__(self):
return u"%s" % self.name
class Meta:
permissions = (
# name
('read_name_article', "Read article's name"),
('change_name_article', "Change article's name"),
# author
('read_author_article', "Read article's author"),
('change_author_article', "Change article's author"),
)
-- serializers.py:
class ArticleSerializer(serializers.ModelSerializer):
class Meta(object):
model = Article
fields = "__all__"
def to_representation(self, request_data):
# get the original representation
ret = super(ArticleSerializer, self).to_representation(request_data)
current_user = self.context['request'].user
for field_name, field_value in sorted(ret.items()):
if not current_user.has_perm(
'app_name.read_{}_article'.format(field_name)
):
ret.pop(field_name) # remove field if it's not permitted
return ret
def to_internal_value(self, request_data):
errors = {}
# get the original representation
ret = super(ArticleSerializer, self).to_internal_value(request_data)
current_user = self.context['request'].user
for field_name, field_value in sorted(ret.items()):
if field_value and not current_user.has_perm(
'app_name.change_{}_article'.format(field_name)
):
errors[field_name] = ["Field not allowed to change"] # throw error if it's not permitted
if errors:
raise ValidationError(errors)
return ret
For a solution that allows both reading and writing, do this:
class PrivateField(serializers.Field):
def get_attribute(self, obj):
# We pass the object instance onto `to_representation`,
# not just the field attribute.
return obj
def to_representation(self, obj):
# for read functionality
if obj.created_by != self.context['request'].user:
return ""
else:
return obj.private_field1
def to_internal_value(self, data):
# for write functionality
# check if data is valid and if not raise ValidationError
class UserInfoSerializer(serializers.HyperlinkedModelSerializer):
private_field1 = PrivateField()
...
See the docs for an example.
This is an old question, but the topic is still relevant.
DRF recommends to create different serializers for different permission. But this approach only works, if you have only a few permissions or groups.
restframework-serializer-permissions is a drop in replacement for drf serializers.
Instead of importing the serializers and fields from drf, you are importing them from serializer_permissions.
Installation:
$ pip install restframework-serializer-permissions
Example Serializers:
# import permissions from rest_framework
from rest_framework.permissions import AllowAny, IsAuthenticated
# import serializers from serializer_permissions instead of rest_framework
from serializer_permissions import serializers
# import you models
from myproject.models import ShoppingItem, ShoppingList
class ShoppingItemSerializer(serializers.ModelSerializer):
item_name = serializers.CharField()
class Meta:
# metaclass as described in drf docs
model = ShoppingItem
fields = ('item_name', )
class ShoppingListSerializer(serializers.ModelSerializer):
# Allow all users to list name
list_name = serializers.CharField(permission_classes=(AllowAny, ))
# Only allow authenticated users to retrieve the comment
list_comment = serializers.CharField(permissions=(IsAuthenticated, ))
# show owner only, when the current user has 'auth.view_user' permission
owner = serializers.CharField(permissions=('auth.view_user', ), hide=True)
# serializer which is only available, when the user is authenticated
items = ShoppingItemSerializer(many=True, permissions=(IsAuthenticated, ), hide=True)
class Meta:
# metaclass as described in drf docs
model = ShoppingItem
fields = ('list_name', 'list_comment', 'owner', 'items', )
Disclosure: I'm the author of this extension
In case you are performing only READ operations, you can just pop the fields in to_representation method of the serializer.
def to_representation(self,instance):
ret = super(YourSerializer,self).to_representation(instance)
fields_to_pop = ['field1','field2','field3']
if instance.created_by != self.context['request'].user.id:
[ret.pop(field,'') for field in fields_to_pop]
return ret
This should be enough to hide sensitive fields.
Just share another possible solution
For example, to make email only show for oneself.
On UserSerializer, add:
email = serializers.SerializerMethodField('get_user_email')
Then implement get_user_email like this:
def get_user_email(self, obj):
user = None
request = self.context.get("request")
if request and hasattr(request, "user"):
user = request.user
return obj.email if user.id == obj.pk else 'HIDDEN'
I solved it using a serializer Mixin:
class FieldPermissionModelSerializerMixin(serializers.ModelSerializer):
"""
A mixin that allows you to specify what fields will be returned based on field level permissions
"""
permission_fields = []
def get_field_names(self, declared_fields, info) -> List:
"""Determine the fields to apply."""
fields = getattr(self.Meta, "fields", [])
for permission_field in self.permission_fields:
app_name = getattr(self.Meta, "model", None)._meta.app_label
permission_name = f"can_view_field_{permission_field}"
full_permission_name = f"{app_name}.{permission_name}"
if self.context["request"].user.has_perm(full_permission_name):
fields.append(permission_field)
return fields
Then you can use this serializer with base fields and permissionable fields.
POSITION_BASE_FIELDS = [
"id",
"name",
"level",
"role",
"sort",
]
POSITION_PERMISSION_FIELDS = ["market_salary", "recommended_rate_per_hour"]
class PositionListSerializer(FieldPermissionModelSerializerMixin):
permission_fields = POSITION_PERMISSION_FIELDS
class Meta:
model = Position
fields = POSITION_BASE_FIELDS + []
This is then based on field level permissions defined on the model.
class Position(models.Model):
name = models.CharField(max_length=255, db_index=True)
level = models.CharField(max_length=255, null=True, blank=True)
sort = models.IntegerField(blank=True, default=0)
market_salary = models.DecimalField(max_digits=19, decimal_places=2, default=0.00)
recommended_rate_per_hour = models.DecimalField(
max_digits=7, decimal_places=2, null=True, blank=True
)
class Meta:
ordering = ["name", "sort"]
unique_together = ("name", "level")
permissions = (
("can_view_field_market_salary", "Can view field: market_salary"),
(
"can_view_field_recommended_rate_per_hour",
"Can view field: recommended_rate_per_hour",
),
)