Django update modelform with field constraints - django

Having the following Model:
class Book(models.Model):
name = models.CharField()
author = models.CharField()
date = models.DateField()
class Meta:
unique_together = ('name', 'author')
class BookSerializerWrite(serializers.ModelSerializer):
class Meta:
model = Book
class BookView(ApiView):
def put(self, request, *args, **kwargs):
serializer = BookSerializerWrite(data=request.data)
if serializer.is_valid():
serializer.save()
The view above does not work as the serializer.is_valid() is False.
The message is:
'The fields name, author must make a unique set'
Which is the constraint of the model.
How do I update the model?
I would rather not override the serializer's validation method.
I also cannot access the validated_data for an update as in
https://www.django-rest-framework.org/api-guide/serializers/#saving-instances
as this is empty due to the fact that the form does not validate.
Is there a builtin solution?

You can achieve it using UpdateAPIview
serializers.py
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('name', 'author', 'date')
views.py
from rest_framework.generics import UpdateAPIview
from .serializers import BookSerializer
class BookUpdateView(UpdateAPIView):
serializer_class = BookSerializer
urls.py
from django.urls import path
from . import views
url_patterns = [
path('api/book/<int:pk>/update/', views.BookUpdateView.as_view(), name="book_update"),
]
Now, post your data to above url. It should work.
Reference: https://github.com/encode/django-rest-framework/blob/master/rest_framework/generics.py

Related

Django DRF serializer method field on many to many running 2n queries

I'm using Django 2.2 and Django REST Framework.
I have the following model structure
class MyModel(models.Model):
name = models.ChartField(max_length=200)
class Tag(models.Model):
name = models.ChartField(max_length=50, unique=True)
class MyModelRelation(models.Model):
obj = models.ForeignKey(MyModel, related_name='relation')
user = models.ForeignKey(User)
tags = models.ManyToManyField(Tag)
def tag_list(self):
return self.tags.all().values_list('name', flat=True).distinct()
I want to get the tags list with the MyModel instance and for that, the serializer is
class MyModelSerializer(serializers.ModelSerializer):
tags_list = serializers.SerializerMethodField(read_only=True)
def get_tags_list(self, obj):
return obj.relation.tag_list()
class Meta:
fields = [
'name',
'tags_list'
]
and the view is
class ObjListView(ListAPIView):
serializer_class = MyModelSerializer
def get_queryset(self):
return super().get_queryset().select_related('relation').prefetch_related('relation__tags')
But to get 58 records, it is running almost 109 queries.
The my_app_mymodel`, `my_app_mymodelrelation_tags is repeated multiple times
This is how I suggest you solve the problem. Instead of extracting the name in the DB level, you can do it in the serializer level. It will make things way easier and faster. First, remove the tag_list method from the model class. First add the annotation to your views:
from django.db.models import F
def get_queryset(self):
return super().get_queryset().annotate(tags_list=F('relation__tags')).select_related('relation')
Then in your serializers
class MyModelSerializer(serializers.ModelSerializer):
tags_list = serializers.SlugRelatedField(many=True, slug_field='name', read_only=True)
...

Django DRF add request.user to modelserializer

I am using django rest framework, and I have an object being created via a modelviewset, and a modelserializer. This view is only accessible by authenticated users, and the object should set its 'uploaded_by' field, to be that user.
I've read the docs, and come to the conclusion that this should work
viewset:
class FooViewset(viewsets.ModelViewSet):
permission_classes = [permissions.IsAdminUser]
queryset = Foo.objects.all()
serializer_class = FooSerializer
def get_serializer_context(self):
return {"request": self.request}
serializer:
class FooSerializer(serializers.ModelSerializer):
uploaded_by = serializers.PrimaryKeyRelatedField(
read_only=True, default=serializers.CurrentUserDefault()
)
class Meta:
model = Foo
fields = "__all__"
However, this results in the following error:
django.db.utils.IntegrityError: NOT NULL constraint failed: bar_foo.uploaded_by_id
Which suggests that "uploaded_by" is not being filled by the serializer.
Based on my understanding of the docs, this should have added the field to the validated data from the serializer, as part of the create method.
Clearly I've misunderstood something!
The problem lies in the read_only attribute on your uploaded_by field:
Read-only fields are included in the API output, but should not be
included in the input during create or update operations. Any
'read_only' fields that are incorrectly included in the serializer
input will be ignored.
Set this to True to ensure that the field is used when serializing a
representation, but is not used when creating or updating an instance
during deserialization.
Source
Basically it's used for showing representation of an object, but is excluded in any update and create-process.
Instead, you can override the create function to store the desired user by manually assigning it.
class FooSerializer(serializers.ModelSerializer):
uploaded_by = serializers.PrimaryKeyRelatedField(read_only=True)
def create(self, validated_data):
foo = Foo.objects.create(
uploaded_by=self.context['request'].user,
**validated_data
)
return foo
DRF tutorial recommend to override perform_create method in this case and then edit serializer so, that it reflect to new field
from rest_framework import generics, serializers
from .models import Post
class PostSerializer(serializers.HyperlinkedModelSerializer):
author = serializers.ReadOnlyField(source='author.username')
class Meta:
model = models.Post
fields = ['title', 'content', 'author']
class ListPost(generics.ListCreateAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
def perform_create(self, serializer):
return serializer.save(author=self.request.user)
Cleaner way:
class PostCreateAPIView(CreateAPIView, GenericAPIView):
queryset = Post.objects.all()
serializer_class = PostCreationSerializer
def perform_create(self, serializer):
return serializer.save(author=self.request.user)
class PostCreationSerializer(serializers.ModelSerializer):
author = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Post
fields = ("content", "author")

Could not resolve URL for hyperlinked relationship using view name ( django-rest-framework )

Problem :
I am getting an error like this .
ImproperlyConfigured at /api/users/
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.
I read this post but it didn't work.
serializers.py
class UserSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='user-detail',
lookup_field='profile')
class Meta:
model = User
fields = ('id', 'username', 'first_name', 'last_name', 'url')
class UserProfileSerializer(serializers.ModelSerializer):
user = serializers.ReadOnlyField(source='user.username')
class Meta:
model = UserProfile
fields = "__all__"
# user = serializers.ReadOnlyField(source='owner.username')
def create(self, validated_data):
pass
def update(self, instance, validated_data):
pass
urls.py
user_profile_list = UserProfileViewSet.as_view({
'get': 'list',
'post': 'create'
})
user_profile_detail = UserProfileViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
})
user_list = UserViewSet.as_view({
'get': 'list'
})
user_detail = UserViewSet.as_view({
'get': 'retrieve'
})
user_profiles_detail = UserViewSet.as_view({
'get': 'profile'
})
router = DefaultRouter()
router.register(r'userprofiles', views.UserProfileViewSet)
router.register(r'users', views.UserViewSet)
urlpatterns = [
url(r'^', include(router.urls))
]
views.py
class UserProfileViewSet(viewsets.ModelViewSet):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.
"""
queryset = UserProfile.objects.all()
serializer_class = UserProfileSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
pagination_class = LimitTenPagination
#detail_route(renderer_classes=[renderers.JSONRenderer])
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
This viewset automatically provides `list` and `detail` actions
"""
queryset = User.objects.all()
serializer_class = UserSerializer
Snippet from my models.py
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
What I tried :
I tried changing user-detail to api:user-detail ( yes api namespace do exist in the main urls.py file )
Looking at the docs, I believe you have to use HyperLinkedRelatedField. Also, see this related SO post.
You are confusing arguments in your serializer field definition. What I believe should be:
class UserSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedRelatedField(view_name='api:userprofile-detail',
source='profile')
Edit :
Added namespace api
Remove lookup_filed attribute from your HyperlinkedIdentityField since you do not need it.
Hence your serializer class should instead look like below:
class UserSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='user-detail', source='profile',)
class Meta:
model = User
fields = ('id', 'username', 'first_name', 'last_name', 'url')
The attribute lookup_filed is only needed when referring to a model class with relationship-field/attribute used as a primary key instead of the default autoincrement. For example, it would have been relevant to use it if your model class looked like below:
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile', primary_key=True)
If you're extending the GenericViewSet and ListModelMixin classes, and have the same error when adding the url field in the list view, it's because you're not defining the detail view. Be sure you're extending the RetrieveModelMixin mixin:
class UserViewSet (mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
Credits to an answer from Rowinson Gallego

Django Rest Framework - "Got a `TypeError` when calling `Note.objects.create()'"

When i am trying to POST using the browsable API of DRF, i get the following error:
Got a TypeError when calling Note.objects.create(). This may be
because you have a writable field on the serializer class that is not
a valid argument to Note.objects.create(). You may need to make the
field read-only, or override the NoteSerializer.create() method to
handle this correctly.
I don't know what is generating this error or how to overcome it. Literally spent hours google searching or changing the code. Can someone explain how to overcome this error? (Please note i am new to Django and DRF!)
Here is the models.py:
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
import uuid
class Stock(models.Model):
'''
Model representing the stock info.
'''
user = models.ForeignKey(User)
book_code = models.CharField(max_length=14, null=True, blank=True)
def __str__(self):
return self.book_code
class Note(models.Model):
'''
Model representing the stock note.
'''
user = models.ForeignKey(User)
note = models.TextField(max_length=560)
stock = models.ForeignKey(Stock, related_name='notes')
date_note_created = models.DateTimeField(default=timezone.now)
def __str__(self):
return self.note
This is the views.py:
from rest_framework import generics
from stocknoter.models import Stock, Note
from api.serializers import StockSerializer, NoteSerializer
# Create your views here.
class StockList(generics.ListCreateAPIView):
serializer_class = StockSerializer
def get_queryset(self):
user = self.request.user
return Stock.objects.filter(user=user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
def perform_update(self, serializer):
serializer.save(user=self.request.user)
class NoteList(generics.ListCreateAPIView):
serializer_class = NoteSerializer
def get_queryset(self):
user = self.request.user
return Note.objects.filter(user=user)
def perform_create(self, serializer):
serializer.save(data=self.request.data)
def perform_update(self, serializer):
serializer.save(data=self.request.data)
This is the serializers.py:
from stocknoter.models import Stock, Note
from rest_framework import serializers
class StockSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
notes = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Stock
fields = ('id', 'user', 'book_code', 'notes')
class NoteSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Note
fields = ('user', 'note', 'stock')
I think this is because you are providing keyword argument data to save method. This is unnecessary. save() dont have such argument. Try just this:
def perform_create(self, serializer):
serializer.save()
take a print() of serializer_data in the view.py
to see what you actually sending to model and create.
then in your serializer.py override the create method..
for example:
def post(self, request):
print(serializer_data.validated_data)
def create(self, validated_data):
del validated_data['c']
return X.objects.create(a=validated_data['a'],
b=validated_data['b'],
)

How to automatically hide all slug fields in ModelForm based forms in Django

I have a model with SlugField. Value of that field is created when the model instance is saved for the first time:
from django.db import models
from django.template.defaultfilters import slugify as default_slugify
class SlugModel(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(max_length=100)
def save(self, *args, **kwargs):
if not self.pk:
self.slug = self.slugify(self.name)
return super(SlugModel, self).save(*args, **kwargs)
def slugify(self, tag):
slug = default_slugify(tag)
return slug
If i use that model in ModelForm the slug field is by default displayed.
from django.forms import ModelForm
class SlugModelForm(ModelForm):
class Meta:
model = SlugModel
How to automatically prevent all ModelForms from displaying of all of it's SlugFields without manually specifying ModelForm.exclude or SlugField(editable=False) on each form/field?.
I think you could also extend Lychas response, create a base class and inherit from that one:
class MyModelForm(ModelForm):
class Meta:
exclude = ('slug',)
abstract = True
class AnyForm(MyModelForm):
#more here
This is untested though.
You can exclude fields in the Meta class by assigning field names to exclude:
class Meta:
model = SlugModel
exclude = ('slug',)