How can I change HyperLinkedModelSerializer's default <pk> lookup_url_kwarg? - django-rest-framework

I want to use HyperLinkedModelSerializer in order to add a url field for my Book model. Here is the solution you'd typically find in the average tutorial:
# serializers.py
class BookSerializer(HyperLinkedModelSerializer):
class Meta:
model = Book
fields = ("title", "url",)
# views.py
class BookView(RetrieveAPIView):
serializer_class = BookSerializer
# urls.py
urlpatterns = [
path("<pk>/", BookDetailView.as_view(), name="book-detail"),
]
And that works all right. But now I need to change the URL conf in order to match the book id, not with <pk>, but with <fiction_id>. So I figured I'd just change it!
# urls.py
urlpatterns = [
path("<fiction_id>/", BookDetailView.as_view(), name="book-detail"),
]
Now comes the crash:
django.core.exceptions.ImproperlyConfigured: Could not resolve URL for hyperlinked relationship using view name "fiction-detail". You may have failed to include the related model in your API, or incorrectly configured the `lookup_field` attribute on this field.
I've tried fiddling with the lookup_field, lookup_url_kwargs in my view:
# views.py
class BookView(RetrieveAPIView):
serializer_class = BookSerializer
lookup_field = "pk"
lookup_url_kwargs = "fiction_id"
I've tried reminding the serializer to actually look for the (default) "pk":
# serializers.py
class BookSerializer(HyperLinkedModelSerializer):
class Meta:
model = Book
fields = ("title", "url",)
extra_kwargs = {
"url": {"lookup_field": "pk"},
}
I've tried combinations of these, to no avail. It looks like you can't use anything but <pk> if you want to take advantage of HyperLinkedModelSerializer's url field. The documentation doesn't seem to offer a way to change that behaviour:
By default hyperlinks are expected to correspond to a view name that matches the style '{model_name}-detail', and looks up the instance by a pk keyword argument.
How can I change this behaviour, or is it bound to become too messy?

You should check HyperlinkedModelSerializer implementation and see that it uses a serializer_related_field defaulting to HyperlinkedRelatedField
class HyperlinkedModelSerializer(ModelSerializer):
"""
A type of `ModelSerializer` that uses hyperlinked relationships instead
of primary key relationships. Specifically:
* A 'url' field is included instead of the 'id' field.
* Relationships to other instances are hyperlinks, instead of primary keys.
"""
serializer_related_field = HyperlinkedRelatedField
...
And then HyperlinkedRelatedField has a class attribute lookup_field defaulting to pk
class HyperlinkedRelatedField(RelatedField):
lookup_field = 'pk'
...
What you can do is to use a custom HyperlinkedRelatedField with your own lookup_field
from rest_framework.relations import HyperlinkedRelatedField
from rest_framework.serializers import HyperlinkedModelSerializer
class BookHyperlinkedRelatedField(HyperlinkedRelatedField):
lookup_field = 'fiction_id'
class BookSerializer(HyperLinkedModelSerializer):
serializer_related_field = BookHyperlinkedRelatedField
class Meta:
model = Book
fields = ("title", "url",)

In order to do this, you need to give the url field's new name for the lookup in the matched pattern by passing it through the extra_kwargs dictionary:
# serializers.py
class BookSerializer(HyperLinkedModelSerializer):
class Meta:
model = Book
fields = ("title", "url",)
extra_kwargs = {
"url": {"lookup_url_kwarg": "fiction_id"},
}
Also remember to modify the corresponding view:
# views.py
class BookView(RetrieveAPIView):
serializer_class = BookSerializer
lookup_url_kwarg = "fiction_id"
Do not write lookup_url_kwargs in plural.
You don't need to meddle with the lookup_field at any level as long as the lookup will be done on the model's primary key.

Related

Django Rest Framework - Updating a ForeignKey Field entry in the view

In my Django Rest Framework project, I have a ForeignKey relationship between two models:
class Book(models.Model):
...
category = models.ForeignKey(Category, on_delete=models.CASCADE, blank=True, null=True)
...
class Category(models.Model):
name = models.CharField(max_length=100, blank=False)
As you can see, a Book can belong to a Category but it does not have to. That means the 'category' field could be null.
So, in my views.py, any Book instance can be updated/patched if the user wants to assign a certain Book to a particular Category. That views.py update method looks like this:
class UpdateBooksCategory(generics.GenericAPIView):
'''
Class-based view to update the 'category' field of a Book instance.
'''
serializer_class = BookSerializer
permission_classes = [IsAuthenticated]
def patch(self, request,*args, **kwargs):
# get the Book instance first
book = Book.objects.get(pk=request.data.get('bookId'))
# if it is not assigned to a Category, then assign it
if book and not book.category:
book.category = Category.objects.get(name=request.data.get('categoryName'))
book.save()
serializer = self.get_serializer(book, context={"request": request})
return Response(serializer.data)
# otherwise, return a generic response
return Response({'response': "You have already put the selected Book in a Category."})
If you can see, first I get the Book instance that the user wants to update by using the Book's ID. If its Category field is not already filled, I get a Category instance using the given category name and assign it.
For the sake of completeness, here are my serializer classes:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['id', 'name']
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['id', /*some other fields*/,..., 'category']
So, finally my question: I wanted to know if this is the preferred way of updating a ForeingKey field like this? I mean looking at the UpdateBooksCategory class-based view, is this the right way of doing it? The code works ( I tested it with PostMan) but since I am new to DRF I wanted to know if such an updating process is correct.
You can change your BookSerializer:
class BookSerializer(serializers.ModelSerializer):
category_id = serializers.IntegerField(write_only=True)
category = CategorySerializer(read_only=True)
class Meta:
model = Book
fields = [
'id',
# some other fields,
'category',
'category_id',
]
category will be a nested data that is read only, then setting the category will be by including the category_id in your requests.

Incorrect view name for nested hyperlinks using drf-nested-routers

I am attempting to use drf-nested-routers to create a simple nested Django REST API given the following models:
# models.py
class Podcast(models.Model):
title = models.CharField(max_length=125)
class Episode(models.Model):
podcast = models.ForeignKey(Podcast, on_delete=models.CASCADE)
title = models.CharField(max_length=125)
Based on the readme domains/nameservers example, I have the following routers defined, expecting a URL structure like:
/podcasts[/{podcast_pk}[/episodes[/{episode_pk}]]]
# urls.py
router = routers.DefaultRouter()
router.register(r'podcasts', PodcastViewSet)
episode_router = routers.NestedDefaultRouter(
router, r'podcasts', lookup='podcast'
)
episode_router.register(
r'episodes', EpisodeViewSet, basename='podcast-episodes'
)
# serializers.py
class PodcastSerializer(HyperlinkedModelSerializer):
episodes = HyperlinkedIdentityField(
view_name='podcast-episodes-list',
lookup_url_kwarg='podcast_pk'
)
class Meta:
model = Podcast
fields = '__all__'
class EpisodeSerializer(NestedHyperlinkedModelSerializer):
parent_lookup_kwargs = {
'podcast_pk': 'podcast__pk',
}
class Meta:
model = Episode
fields = '__all__'
# exclude = ('url',) # works without reference to self
# views.py
class PodcastViewSet(viewsets.ModelViewSet):
queryset = Podcast.objects.all()
serializer_class = PodcastSerializer
class EpisodeViewSet(viewsets.ModelViewSet):
serializer_class = EpisodeSerializer
def get_queryset(self):
return Episode.objects.filter(
podcast=self.kwargs['podcast_pk'],
)
Accessing /podcasts/1/episodes/, when url is included, raises the following error:
Exception Type: ImproperlyConfigured at /podcasts/1/episodes/
Exception Value: Could not resolve URL for hyperlinked
relationship using view name "episode-detail". You may
have failed to include the related model in your API,
or incorrectly configured the `lookup_field` attribute
on this field.
Why does it not identify the correct view name, or is there something else obvious I am missing
In my case using 'id' instead of 'pk' is solved my problem.
parent_lookup_kwargs = {
'podcast_pk': 'podcast_id',
}

Django rest framework multi searching

everyone. I tried understand search.
I have url path('quiz/all/', QuizListView.as_view()),
View :
class QuizListView(generics.ListAPIView):
queryset = Quiz.objects.all()
serializer_class = QuizDetailSerializer
search_fields = ('description', 'title',)
filterset_fields = ['title', 'description',]
(method1)If I use search, for example /api/v1/quiz/all/?search=QI got all instances where title or description contains 'Q'
(method2)I can search /api/v1/quiz/all/?title=Q&description=d I got a instance which has exact title and description.
(method3)But I want to get list of all instances where title contains one value and description contains other value. For example, I want to write /api/v1/quiz/all/?title=Q&description=d and get list where title contains Q and description contains d.
Quiz1(title=Q, description=d)
Quiz2(title=Test, description=dd)
Quiz3(title=NewQ, description=Test_d)
For (method1,/api/v1/quiz/all/?search=Q) I got Quiz1, Quiz2, Quiz3
For (method2,/api/v1/quiz/all/?title=Q&description=d) I got Quiz1
For (method3,/api/v1/quiz/all/?title=Q&description=d) I would like to
get Quiz1 and Quiz3 (because they contain Q for title and d for description)
Thanks.
You'll need to create a custom FilterSet class and use the contains or icontains (if you want case insensitive) because the default is using exact and that's why you don't get back the result that you want. See docs here and here
# filters.py
class QuizFilter(django_filters.FilterSet):
class Meta:
model = Quiz
fields = {
'title': ['contains'], # or icontains
'description': ['contains'], # or icontains
}
# views.py
from django_filters.rest_framework import DjangoFilterBackend
from .filters import QuizFilter
class QuizListView(generics.ListAPIView):
queryset = Quiz.objects.all()
serializer_class = QuizDetailSerializer
filter_backends = (DjangoFilterBackend, ) # add here other filters backends
filterset_class = QuizFilter

Multiple endpoints for a single model in REST framework

I have a REST framework app for a multi-page form:
class InformationRequest(models.Model):
# user information
first_name = models.CharField(max_length=60)
last_name = models.CharField(max_length=60)
# contact details
phone = models.CharField(max_length=60)
email = models.CharField(max_length=60)
I'm trying to create endpoints for each of the two blocks of data within the model:
UserInformationSerializer(serializers.Serializer):
first_name = serializers.CharField(max_length=60)
last_name = serializers.CharField(max_length=60)
ContactDetailsSerializer(serializers.Serializer):
phone = serializers.CharField(max_length=60)
email = serializers.CharField(max_length=60)
I'd like the endpoints to look like:
requests/1/user-informtion
requests/1/contact-details
But I'm unsure of how to structure the view to achieve this. Currently I'm using a model viewset:
class InformationRequestViewSet(viewsets.ModelViewSet):
queryset = InformationRequest.objects.all()
serializer_class = ??
Is it possible to have two serializers for one model?
It's certainly possible to have 2 (or any number of) serializers for a model. And you are on the right path. What you want is different urls mapping to different views. So in your case, it can be something like the following:
Note that I turned each of your serializers into a ModelSerializer.
path-to/serializers.py
class UserInformationSerializer(serializers.ModelSerializer):
class Meta:
model = InformationRequest
fields = ('first_name', 'last_name')
class ContactDetailsSerializer(serializers.ModelSerializer):
class Meta:
model = InformationRequest
fields = ('phone', 'email')
Next, we have 2 different urls that point to 2 different views:
path-to/urls.py
urlpatterns = [
url(r'^requests/(?P<pk>\d+)/user-information/$', views.UserInformationDetail.as_view()),
url(r'^requests/(?P<pk>\d+)/contact-details/$', views.ContactInformationDetail.as_view()),
# ... other urls
]
And finally, the views themselves (I'm using generic RetrieveAPIView for convenience)
path-to/views.py
class UserInformationDetail(generics.RetrieveAPIView):
queryset = InformationRequest.objects.all()
serializer_class = UserInformationSerializer
class ContactInformationDetail(generics.RetrieveAPIView):
queryset = InformationRequest.objects.all()
serializer_class = ContactDetailsSerializer

Django Rest Framework - return reverse foreign key property?

I think this is simple and probably a duplicate, but I cannot figure it out by looking at the documentation.
I have Django models as follows:
class Image(models.Model):
manor = models.ForeignKey(Manor, related_name='image_for_manor')
filename = models.CharField(max_length=30, null=True, blank=True)
class Manor(models.Model):
id = models.IntegerField(primary_key=True)
I want the user to be able to query the Manor and see the related Image. I'd like this JSON to be returned:
{
id: 572,
image: 'my/filepath.png'
}
This is my view:
#api_view(['GET'])
def manor(request, id):
mymanor = Manor.objects.get(id=id)
serializer = ManorSerializer(mymanor)
return JSONResponse(serializer.data)
And these are my serializers:
class ImageFilePathSerializer(serializers.ModelSerializer):
class Meta:
model = Image
fields = ('filename',)
class ManorSerializer(serializers.ModelSerializer):
image = ImageFilePathSerializer(source="image_for_manor")
class Meta:
model = Manor
fields = ('id', 'image')
But this doesn't work: I get an empty dictionary for image. (Even if it weren't empty, I realise it wouldn't be right, because I don't want the image property to be a dictionary: I want it to be a string.)
How can I change this to be correct? I cannot work it out.
As Kevin suggested , why dont you use ImageField which will give you url where your image is uploaded. although if you dont want to do that , here are some changes you have to do to get the result format you wanted.(Following solution assumes that only one image will be for one minor)
views.py ( why dont you ImageFilePathSerializer instead of another one as it also contains all the data you wanted)
#api_view(['GET'])
def manor(request, id):
mymanor = Image.objects.get(manor__id=id)
serializer = ImageFilePathSerializer(mymanor)
return Response(serializer.data)
serializers.py (add Id with filename in ImageFilePathSerializer)
class ImageFilePathSerializer(serializers.ModelSerializer):
class Meta:
model = Image
fields = ('id', 'filename',)
class ManorSerializer(serializers.ModelSerializer):
filename = serializers.ImageField(source="image_for_manor")
class Meta:
model = Manor
fields = ('id', 'filename')

Resources