Django REST Framework UniqueTogetherValidator fails with FK from kwargs - python-3.x

I've got myself into a trouble with UniqueTogetherValidator.
The problem is that ReviewSerliazer unlike CommentSerializer which is almost identical does unique together validation before actually getting a title value from kwargs and sends back 400 answer with title field being required. I've tried to identify it as a HiddenField, but although serializer validation goes fine within tests, model validation does not. And I receive django.db.utils.IntegrityError: UNIQUE constraint failed: reviews_review.author_id, reviews_review.title_id
Key idea is that title should not be included in serializer response. Making it PrimaryKeyRelatedField and then doing the to_representation seems odd to me.
Any ideas how to fix this without catching exceptions in the viewset, which is obviously wrong?
models.py
class BasePost(models.Model):
text = models.TextField()
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name='%(class)ss')
pub_date = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ('-pub_date', )
abstract = True
def __str__(self):
return self.text[:30]
class Review(BasePost):
score = models.IntegerField(
default=0, validators=[MaxValueValidator(10), MinValueValidator(1)])
title = models.ForeignKey(
Title, on_delete=models.CASCADE, related_name='reviews')
class Meta(BasePost.Meta):
constraints = [
models.UniqueConstraint(
fields=('author', 'title', ), name='unique_title_review')]
class Comment(BasePost):
review = models.ForeignKey(
Review, on_delete=models.CASCADE, related_name='comments')
urls.py
router_v1.register(
r'^titles/(?P<title_id>\d+)/reviews', ReviewViewSet, basename='review')
router_v1.register(
r'^titles/(?P<title_id>\d+)/reviews/(?P<review_id>\d+)/comments',
CommentViewSet, basename='comment')
views.py
class ReviewViewSet(BasePostViewSet):
serializer_class = ReviewSerializer
def get_queryset(self):
return self.get_title().reviews.all()
def perform_create(self, serializer):
serializer.save(author=self.request.user, title=self.get_title())
def get_title(self, key='title_id'):
return get_object_or_404(Title, id=self.kwargs.get(key))
class CommentViewSet(BasePostViewSet):
serializer_class = CommentSerializer
def get_queryset(self):
return self.get_review().comments.all()
def perform_create(self, serializer):
serializer.save(author=self.request.user, review=self.get_review())
def get_review(self, key='review_id'):
return get_object_or_404(Review, id=self.kwargs.get(key))
serializers.py
class ReviewSerializer(BasePostSerializer):
title = serializers.HiddenField(default=None)
class Meta:
model = Review
fields = ('id', 'text', 'author', 'score', 'pub_date', 'title', )
validators = [UniqueTogetherValidator(
queryset=Review.objects.all(), fields=('author', 'title', ))]
class CommentSerializer(BasePostSerializer):
class Meta:
model = Comment
fields = ('id', 'text', 'author', 'pub_date', )

def create(self, validated_data):
try:
review = Review.objects.create(**validated_data)
except IntegrityError:
raise serializers.ValidationError(
{'detail': 'Вы можете оставить только один отзыв.'})
return review
Currently fixed like that but seems wrong to me.

First, I will assume that BasePostViewSet does inherits from CreateModelMixin. My assumption is based on the fact that you are redefining perform_create:
def perform_create(self, serializer):
serializer.save()
According to DRF Documentation on passing additional argumernts to the .save() method:
Any additional keyword arguments will be included in the validated_data argument when .create() or .update() are called.
The original .create() method from the CreateModelMixin:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
It does the validation before calling the .perform_create() method, hence your redefined version of .perform_create:
def perform_create(self, serializer):
serializer.save(author=self.request.user, review=self.get_review())
Gets called after validation, hence, your serializer validator is not checked with your desired arguments.
Now, I think you could achieve this by including your fields in the to_internal_value function data like this (didn't try it out but definitely it runs before validation) (I'm assuming that serializer context is passed accordingly):
def to_internal_value(self, data):
data['author'] = self.context['request'].user
data['review'] = self.context['view'].get_review()
return data
Hopefully this helps.

Related

Django Create or update with unique together values

I'm trying to make an attendance system in the frontend I retrieve a list of users which the request.user can take attendance of, I'm using CreateAPIView but this won't get me the desired effect as I want the request.user to be able to toggle between Absent, Present, and on_leave even after the entry has been created on the first request
i have seen questions and answers about create or update here but i couldn't use them so any help would be appriciated
this is my view.py
class AttendanceListCreateAPIView(CreateAPIView):
permission_classes = [IsTeacher_Student]
queryset = Attendance.objects.all()
serializer_class = AttendanceSerializer
def post(self, request, *args, **kwargs):
user = request.user
data = request.data
serializer = AttendanceSerializer(data=data)
if serializer.is_valid():
data_user = serializer.validated_data['user']
## validation
return Response({"message": "Something is wrong, maybe you have already taken attendance for this user"},
status=status.HTTP_400_BAD_REQUEST)
my serializer.py
class AttendanceSerializer(serializers.ModelSerializer):
date = serializers.HiddenField(default=timezone.now)
leave_reason = serializers.CharField(required=False, default="")
class Meta:
model = Attendance
fields = ['user', 'presence', 'leave_reason', 'date']
extra_kwargs = {
'user': {'required': True},
'presence': {'required': True},
'leave_reason': {'required': False},
}
validators = [
UniqueForYearValidator(
queryset=Attendance.objects.all(),
field='user',
date_field='date',
message=("You have already taken the attendance")
)
]
def create(self, validated_data):
instance = Attendance.objects.create(
user=validated_data['user'],
presence=validated_data['presence'],
leave_reason=validated_data['leave_reason'],
date=validated_data['date'],
)
instance.save()
return instance
desired effect
the user fk and date are unique together meaning that if the date isn't unique for the user update it if it is create a new entry

Django Rest Framework Field Validation Issue

My subscription view is located inside of UserViewSet. I'm wondering why I'm getting
IntegrityError at /api/users/1/subscribe/
new row for relation "users_subscription" violates check constraint "prevent_self_subscription"
DETAIL: Failing row contains (11, 1, 1).
instead of proper json answer. Somehow SubscriptionSerializer field validation doesnt wish to work. Any thoughts?
models.py
class Subscription(models.Model):
user = models.ForeignKey(
User, related_name='subscriber',
on_delete=models.CASCADE)
author = models.ForeignKey(
User, related_name='subscribing',
on_delete=models.CASCADE)
class Meta:
constraints = [
models.UniqueConstraint(
fields=('user', 'author'),
name='unique_subscription'
),
models.CheckConstraint(
check=~models.Q(user=models.F('author')),
name='prevent_self_subscription'
)
]
serializers.py
class SubscriptionSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(
read_only=True, default=serializers.CurrentUserDefault())
class Meta:
model = models.Subscription
fields = ('author', 'user', )
validators = [
serializers.UniqueTogetherValidator(
queryset=models.Subscription.objects.all(),
fields=['author', 'user', ]
)
]
def create(self, validated_data):
return models.Subscription.objects.create(
user=self.context.get('request').user, **validated_data)
def validate_subscribing(self, value):
if self.context.get('request').user == value:
raise serializers.ValidationError(
'You cant subscribe to yourself!')
return value
views.py
#action(['post'], detail=True)
#permission_classes(permissions.IsAuthenticated)
def subscribe(self, request, *args, **kwargs):
author = get_object_or_404(models.User, id=kwargs['id'])
data = request.data.copy()
data.update({'author': author.id})
serializer = serializers.SubscriptionSerializer(
data=data, context={'request': request})
if request.method == 'POST':
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(
status=status.HTTP_201_CREATED,
data=self.get_serializer(author).data)
It was the wrong method name, field name is 'author' and method was validate_subscribing() instead of validate_author().

DRF ViewSet extra action (`#action`) serializer_class

When I try to use Django Rest Framework extra actions on a viewset, I can not make the decorator's serializer_class work.
class ClientViewSet(ModelViewSet):
queryset = Client.objects.all()
serializer_class = ClientSerializer
def get_queryset(self):
# Do things
def get_serializer_class(self):
if self.action in ["create"]:
return CreateClientSerializer
elif self.action in ["retrieve"]:
return ClientDetailSerializer
return self.serializer_class
#action(detail=True, methods=["get"], serializer_class=ClientDetailSerializer)
def get_by_name(self, request, name=None):
"""
Get one Client searching by name.
#param request:
#param name: Client code
#return: Response
"""
queryset = get_object_or_404(Client, name__iexact=name)
serializer = self.get_serializer(queryset)
return Response(serializer.data)
So, even if the extra action is supposedly overriding the ViewSet default serializer class, I still get ClientSerializer instead of ClientDetailSerializer.
The official documentation states that...
The decorator allows you to override any viewset-level configuration such as permission_classes, serializer_class, filter_backends...:
My get_serializer_class override defaults to the ViewSet serializer_class attribute for my extra actions. If I understand correctly, this is basically what GenericAPIView get_serializer_class does under the hood:
def get_serializer_class(self):
"""
(...)
"""
assert self.serializer_class is not None, (
"'%s' should either include a `serializer_class` attribute, "
"or override the `get_serializer_class()` method."
% self.__class__.__name__
)
return self.serializer_class
I guess I'm missing something obvious here. Just can not figure out what...
Any help is appreciated. Thanks in advance :)
Why not use it like this? I'm guessing you're doing something wrong in get_serializer_class.
#action(detail=True, methods=["get"], serializer_class=ClientDetailSerializer)
def get_by_name(self, request, name=None):
"""
Get one Client searching by name.
#param request:
#param name: Client code
#return: Response
"""
object = get_object_or_404(Client, name__iexact=name)
serializer = ClientDetailSerializer(object)
return Response(serializer.data)
When you override the get_serializer_class without calling the super of this class, the super class doesn't run.
user this:
def get_serializer_class(self):
if self.action in ["create"]:
return CreateClientSerializer
elif self.action in ["retrieve"]:
return ClientDetailSerializer
return super().get_serializer_class()

Django rest frame work; Combining data from different models doesnot work

I tried combining data from two models. the serializers look like this.
class FindOwnerSaveSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'first_name', 'last_name','image')
#fields = ('__all__')
class SaveSerializer(serializers.ModelSerializer):
content = FindOwnerSaveSerializer(source='user',read_only=True)
class Meta:
model = Save
fields = ('project_id','content')
The save model looks like this
class Save(models.Model):
project_id = models.ForeignKey(Project, on_delete=models.DO_NOTHING)
user_id = models.ForeignKey(User, on_delete=models.DO_NOTHING)
and the view looks like this
class SaveView(UpdateAPIView):
serializer_class = SaveSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
try:
return Save.objects.filter(user_id=self.request.user)
except Exception as e:
logger.error(e)
return Response(data='false')
def get_object(self):
try:
if Project.objects.get(project_id=self.request.data['project_id']):
return Response(data='true')
except Exception as e:
logger.error(e)
return Response(data='false ')
the response keep looking like this. No user data is comming in.
[
{
"project_id": 78
}
]
What could be the problem.Iam using django 2.2.7 and rest framwework 3.10.3.
The problem with your source referrence, it should be source='user_id' not source='user'
class SaveSerializer(serializers.ModelSerializer):
content = FindOwnerSaveSerializer(source='user_id',read_only=True) # not `source='user'`
class Meta:
model = Save
fields = ('project_id','content')

Django RestFramework call the Api by the Field

currently i pass the parameter 'id' in the url and i call the API by id however i would like to call API by the parameter.here is views.py`
class post_list(APIView):
def get(self,request,format=None):
post_resource=PostResource()
dataset=Dataset()
post = Post.objects.all()
serializer = PostSerializer(post, many=True)
a=[]
for row in post:
a.append(row)
return Response(serializer.data)
def post(self,request,format =None):
serializer = PostSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)
class post_detail(APIView):
def get_object(self, pk):
try:
return Post.objects.get(pk=pk)
except Post.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
post = self.get_object(pk)
serializer = PostSerializer(post)
return Response(serializer.data)
def put(self, request, pk, format=None):
snippet = self.get_object(pk)
serializer = PostSerializer(snippet, 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):
post = self.get_object(pk)
post.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
this is the result when i pass the id in the url`
when i pass the id in the url it's show only the that particular id result however i need view if i pass the other field instead of the id it's show the result based on that.so my question is what should i have to change in the views.py and Urls.py. this is urls.py file`
from django.conf.urls import url
from api import views
from rest_framework.urlpatterns import format_suffix_patterns
urlpatterns = [
url(r'^post/$', views.post_list.as_view()),
url(r'^post/(?P<pk>[0-9]+)/$', views.post_detail.as_view()),
]
urlpatterns=format_suffix_patterns(urlpatterns)
This is my serializer.py file`
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
#fields=('ProductName','Score')
fields ='__all__'
`
You should change your url so that it accepts string:
url(r'^post/(?P<pname>[a-zA-Z0-9]+)/$', views.post_detail.as_view())
and then change your get_object method like this:
def get_object(self, pname):
try:
return Post.objects.get(pname=pname)
except Post.DoesNotExist:
raise Http404
But you will issues if pname is not unique, because it will complain that you have more than one such Post.
Hope it helps!

Resources