Django unit test IntegrityError handling - django

I have a TransactionManagementError in my Django unittest attemption.
Here is my model:
class Like(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
post = models.ForeignKey('Post', on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
class Meta:
constraints = [models.UniqueConstraint(fields=['user', 'post'])]
Here is my testing view:
class LikeCreateDestroyAPIView(APIView):
permission_classes = (permissions.IsAuthenticated,)
def get(self, *args, **kwargs):
try:
like = Like.objects.create(user=self.request.user, post=Post.objects.get(pk=self.kwargs['post_id']))
reply = 'like created'
status = 201
except IntegrityError:
like_to_delete = Like.objects.get(user=self.request.user, post=Post.objects.get(pk=self.kwargs['post_id']))
like = copy.deepcopy(like_to_delete)
like_to_delete.delete()
reply = 'like deleted'
status = 204
except Post.DoesNotExist:
return Response({'status': "Post doesn't exist"}, status=404)
return Response({reply: model_to_dict(like)}, status=status)
Here is my unit test inherits from rest_framework.test.APITestCase.
I try to get like by already liked user (successfully added like with that user before test) so it violates integrity constraint of the Like model.
# Authorized by liked user
response3 = self.client.get(
reverse("like-create-api", args=[self.test_post_11.id]),
HTTP_AUTHORIZATION=f"JWT {self.access_token_user1}"
)
self.assertEqual(response3.status_code, 204)
Can somebody help with any advise?

So I just changed LikeCreateDestroyAPIView to avoid IntegrityError. So now the test is passed. But anyway it's interesting why the test violates the error and how to solve such an exception in testing?

Related

How to clear a ManyToMany Field in a patch request?

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')

Getting django.db.utils.OperationalError: sub-select returns 11 columns - expected 1 when trying to add an object to a m2m field

I am new to django rest framework, and I am trying to build api for a todo app using which i can add collaborator to a todo my todo model is as follows:
class Todo(models.Model):
creator = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=255)
collaborators = models.ManyToManyField(User,related_name='collaborators')
def __str__(self):
return self.title
Serializer.py
class TodoCreateSerializer(serializers.ModelSerializer):
def save(self, **kwargs):
data = self.validated_data
user = self.context['request'].user
title = data['title']
todo = Todo.objects.create(creator=user, title=title)
return todo
class Meta:
model = Todo
fields = ('id', 'title',)
class UserObjectSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username']
class TodoSerializer(serializers.ModelSerializer):
collaborators = UserObjectSerializer(many=True, required = False)
class Meta:
model = Todo
fields = ['id', 'title', 'collaborators']
class CollaboratorSerializer(serializers.ModelSerializer):
collaborators = UserObjectSerializer(many=True)
class Meta:
model = Todo
fields = ['collaborators']
and when i try to add collaborator object to my collaborator field using the following method in the viewset
class CollaboratorViewSet(viewsets.ModelViewSet):
serializer_class = CollaboratorSerializer
queryset=Todo.objects.all()
#action(methods=['put'], detail=True, permission_classes=[permissions.IsAuthenticated, ], url_path='add-collaborator', url_name='add_collaborator')
def add_collaborator(self, request, pk=None):
try:
todo = Todo.objects.get(pk=pk)
except Exception:
return Response({'Error':'Invalid Id'})
else:
if request.user == todo.creator:
try:
collborators = request.data["collaborators"]
except KeyError:
return Response({'Error':'Collaborator field not specified in request'}, status = status.HTTP_400_BAD_REQUEST)
else:
collab_list = []
for collab in collborators:
try:
collab_name = collab['username']
except KeyError:
return Response({'Error':'No username provided'}, status = status.HTTP_400_BAD_REQUEST)
else:
new_collab = User.objects.filter(username=collab_name)
if new_collab:
collab_list.append(new_collab)
else:
return Response({'detail':'No user with provided username exists'}, status = status.HTTP_400_BAD_REQUEST)
todo.collaborators.add(*collab_list)
return Response({'Success':'Added collaborators successfully'}, status=status.HTTP_200_OK)
else:
raise PermissionDenied("You are not the creator of this Todo.")
#action(methods=['patch'], detail=True, permission_classes=[permissions.IsAuthenticated, ], url_path='delete-collaborator', url_name='delete_collaborator')
I get an error
django.db.utils.OperationalError: sub-select returns 11 columns - expected 1
but when i use todo.collaborators.set(collaborator_object)
collaborator gets added but this way i am able to add only one collaborator and no effect occurs when i try to add multiple collaborators.
I know that we can add multiple object to m2m relation fields using .add() method but that's not working when i try to add even one object, i also went through the documentation for .add() method but could not find any help.
Sorry for such a long question and thanks for helping in advance!
I managed to solve the error, actually I was was trying to access the collaborators as
new_collab = User.objects.filter(username=collab_name)
which will return a queryset even if there is a unique user with given username
and then i was trying to add collaborator directly to collaborators field which caused this error as it expected an User instance but I was providing a queryset
to avoid that I just replaced the above line as
new_collab = User.objects.get(username=collab_name)
which will return a User object instance if it exists so no error is raised and I am able to add multiple as well as single collaborators.

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)

In Django 2.0 How do I handle a ProtectedError in generic.DeleteView

I have a generic view declared as follows:
class CustomerDelete(LoginRequiredMixin,DeleteView):
model = models.Customer
success_url = reverse_lazy('customer-list')
And a model declared as follows:
class Order(models.Model):
Customer = models.ForeignKey(Customer, on_delete=models.PROTECT, default=None)
Shop = models.ForeignKey(Shop, on_delete=models.PROTECT, default=None)
Status = models.IntegerField(choices=STATUS);
Reference = models.CharField(max_length=50)
Date = models.DateTimeField(default=None)
LastAuthorizationDate = models.DateTimeField(default=None, null=True)
LastUpdated = models.DateTimeField(auto_now=True)
def get_absolute_url(self):
return reverse_lazy('order-view', None, [self.id])
def type(self):
return 'Order'
def Name(self):
return self.Customer.Name + ' - ' + self.Shop.Name + ' - ' + self.Reference
Upon delete I get the following exception:
ProtectedError at /customer/2/delete/ ("Cannot delete some instances
of model 'Customer' because they are referenced through a protected
foreign key: 'Order.Customer'", ,
, , , ]>)
What would be the best class method to override and catch the exception that would allow me to redirect to the referrer with an error attached?
Thanks in advance.
You need to override the delete method, to add your custom logic:
class CustomerDelete(LoginRequiredMixin,DeleteView):
model = models.Customer
success_url = reverse_lazy('customer-list')
error_url = reverse_lazy('customer-has-orders-error')
def get_error_url(self):
if self.error_url:
return self.error_url.format(**self.object.__dict__)
else:
raise ImproperlyConfigured(
"No error URL to redirect to. Provide a error_url.")
def delete(self, request, *args, **kwargs):
"""
Call the delete() method on the fetched object and then redirect.
"""
self.object = self.get_object()
success_url = self.get_success_url()
error_url = self.get_error_url()
try:
self.object.delete()
return HttpResponseRedirect(success_url)
except models.ProtectedError:
return HttpResponseRedirect(error_url)
If you are going to be using this often, you can create your own custom mixin with the above logic.
In addition, consider implementing a soft delete in your application, so that records are not deleted from the database immediately, but are flagged for deletion at a later date - once they are archived. Otherwise you risk having issues with your business logic.

CreateAPIView with Empty Serializer (no field)

I'm implementing 'Follow' CreateAPIView.
FollowCreateAPIView
- gets 'logged-in User' from self.request.user
- gets 'user's id' who 'Logged-in User' wants to follow (from url)
- with the information above, Follow create new data!
Here is urls.py
url(r'^follow/(?P<user_id>\d+)$', FollowCreateAPIView.as_view(), name="user_profile"),
Model
class Follow(models.Model):
follower = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='follower')
following = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='following')
created_at = models.DateTimeField(auto_now_add=True)
CreateAPIView (I can't get user_id from URL; print(following_id) returns None
)
class FollowCreateAPIView(generics.CreateAPIView):
queryset = Follow.objects.all()
serializer_class = FollowCreateSerializer
def perform_create(self, serializer):
following_id = self.request.GET.get('user_id', None)
print(following_id) <<<<- HERE!
following_user = User.objects.filter(id=following_id)
serializer.save(follower=self.request.user, following=following_user)
Serializer
class FollowCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Follow
fields = ()
Could you fix this problem? And this Like feature seems very common. Do you have any better approach? (if you have...)
Thank you so much!
I think you can get the value with kwargs. Also, you can use get instead of filter as id is incremental and unique.
try:
following_user = User.objects.get(id=self.kwargs['user_id'])
except User.DoesNotExist:
raise Http404
Thanks Risadinha for pointing that out.