wagtail get_api_representation() not called - django-rest-framework

I am using Streamfields in Wagtail and got the following problem:
I would like to print out the image path instead of the id and followed the instructions here:
Custom representation of Streamfield in rest API
But it seems that get_api_representation() is never called in my case.
urls.py
router.register(r'page', PageView, basename='page')
views.py
class PageView(viewsets.mixins.RetrieveModelMixin, GenericViewSet):
queryset = Page
serializer_class = PageSerializer
serializers.py
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = ('title', 'body')
models.py
class APIImageChooserBlock(ImageChooserBlock):
def get_api_representation(self, value, context=None):
if value:
return {
'id': value.id,
'title': value.title,
'image': value.get_rendition('width-1000').attrs_dict
}
class ImageTextBlock(blocks.StructBlock):
image = APIImageChooserBlock()
text = blocks.CharBlock()
class Page(wagtail_models.Page):
body = StreamField(
[
('MyBlock', ImageTextBlock()),
('Newsletter', blocks.StructBlock([
('text', blocks.RichTextBlock()),
])),
]
)
content_panels = wagtail_models.Page.content_panels + [
StreamFieldPanel('body', classname="full", heading=_('Content'))
]
Can somebody identify the problem here?

Related

Django REST error when serializing uploaded image

I'm getting a serializer error:
"Upload a valid image. The file you uploaded was either not an image
or a corrupted image"
When trying to serialize an uploaded image
My code:
Models:
class Post(models.Model):
text = models.TextField(max_length=10000)
class Image(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
image = models.ImageField(blank=False, null=False, upload_to='test')
View:
class CreateNewPostAPIView(generics.CreateAPIView):
serializer_class = serializers.NewPostSerializer
def get_serializer_context(self):
context = super().get_serializer_context()
if self.request.data.get('image', None):
context['image'] = self.request.data.pop('image', None)
return context
Serializers:
class NewPostSerializer(serializers.ModelSerializer):
def create(self, validated_data):
post = Post.objects.create(**validated_data)
image = self.context['image'][0] # working with only one image for now
# image is of type <class'django.core.files.uploadedfile.InMemoryUploadedFile'>
serializer = ImageSerializer(data={'post': post.pk, 'image': image})
is_valid = serializer.is_valid() # it's always False!
return post
class ImageSerializer(serializers.ModelSerializer):
class Meta:
model = Image
fields = ('post', 'image')
I already tried doing that without a serializer and it's working with this code:
class NewPostSerializer(serializers.ModelSerializer):
def create(self, validated_data):
post = Post.objects.create(**validated_data)
image = self.context['image']
Image.objects.create(post=post.pk, image=image)
return post
But I need to do it using a serializer.
The easy way for this would be like:
Models:
from django.db import models
# Create your models here.
class Post(models.Model):
text = models.TextField()
def __str__(self):
return self.text[:50]
class Image(models.Model):
post = models.ForeignKey(
Post, on_delete=models.CASCADE,
related_name='images', editable=False
)
image = models.ImageField(upload_to='images/')
def __str__(self):
return self.image.name
def delete(self, *args, **kwargs):
self.image.delete()
super().delete(*args, **kwargs)
Serializer:
from rest_framework import serializers
from .models import Post, Image
class ImageSerializer(serializers.ModelSerializer):
class Meta:
model = Image
fields = "__all__"
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = "__all__"
class PostWithImageSerializer(serializers.ModelSerializer):
image = serializers.ImageField(write_only=True)
# This is for the GET request or the response of the POST request
# We can also work with a separate serializer for such cases
images = ImageSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = "__all__"
def create(self, validated_data):
image_data = validated_data.pop('image')
post = Post.objects.create(**validated_data)
Image.objects.create(post=post, image=image_data)
return post
Views as:
from rest_framework.response import Response
from rest_framework import status
from .serializers import *
# Create your views here.
class AddPostWithAnImageView(APIView):
serializer_class = PostWithImageSerializer
def post(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
For testing purposes, please use Postman where you can upload images with so much ease. The request form should only require the text and the image fields.
Thanks to #pKiran, who gave me an idea, now I have a working code for saving one or more images. It's, also, quite concise and readable. There's quite a lot going on here with so little of code. Django REST is quite confusing.
class ImageSerializer(serializers.ModelSerializer):
class Meta:
model = Image
fields = ('image', 'thumb')
class NewPostSerializer(serializers.ModelSerializer):
image = serializers.ListField(child=serializers.ImageField(), write_only=True, required=False)
def create(self, validated_data):
images = validated_data.pop('image', None)
post = Post.objects.create(**validated_data)
if images:
img_models = [Image(post=post,
image=image, thumb=make_thumb(image))
for image in images]
Image.objects.bulk_create(img_models)
return post

Confused about reverse URL in testing mode

In my test file, there is one class with :
INGREDIENTS_URL = reverse("recipe:ingredient-list")
def setUp(self):
self.client = APIClient()
def test_Auth_required(self):
res = self.client.get(INGREDIENTS_URL)
I have one recipe app, but there are no "recipe-list" anywhere else.
Ingredient endpoint is defined with a router in the view.py :
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('ingredients', views.IngredientViewSet)
And the serializer.py
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = ['id', 'name']
read_only_fields = ['id']
So I wonder how the reverse is working in this case
EDIT 1
I guess the reverse("recipe:ingredient-list") it related with viewsets.ModelViewSet and would inherit of my views.py :
class IngredientViewSet(mixins.ListModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet):

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',
}

Select one to many relation table in query set django

I would like to select one to many table relation in query set.I have two model.
class Post(models.Model):
postid = models.AutoField(primary_key=True)
class Meta:
db_table = 'post'
class Images(models.Model):
postid = models.ForeignKey(Post,on_delete=models.CASCADE)
image = models.ImageField(upload_to=upload_image_to, blank=True)
In list method of Post viewset, I would like to select format like=>
{
"postid": 1,
"images":[
image:"Image Result"
]
}
Here is Post viewset =>
def list(self, request):
queryset = self.queryset
page = self.paginate_queryset(queryset)
for post in self.queryset:
img = Images.objects.all().filter(postid=post.postid)
serializer = PostSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
How to insert this img result to post?
Take a look at the RelatedField documentation. Something like:
class PostSerializer(serializers.ModelSerializer):
images = serializers.StringRelatedField(many=True)
class Meta:
model = Post
fields = ['postid', 'images']
This will give you a string representation of the Image, but you can look at other Relationship fields in the documentation to customise the JSON representation of the image.
Doing it like this will likely mean you don't need to override list() in the viewset as well.

Django Rest: Serializers Based on Multiple Querysets

I'm working with serializers as described by the link:Serializer relations (section PrimaryKeyRelatedField)
I have a slight different need, I'm sure it's really easy.
class Album(models.Model):
album_name = models.CharField(max_length=100)
artist = models.CharField(max_length=100)
class Track(models.Model):
album = models.ForeignKey(Album, related_name='tracks')
order = models.IntegerField()
title = models.CharField(max_length=100)
duration = models.IntegerField()
class SomeWidget(models.Model):
album = models.ForeignKey(Album)
track = models.ForeignKey(Track)
name = models.CharField(max_length=100)
description = models.CharField(max_length=100)
My need, I need to return the following:
{
'album_name': 'Things We Lost In The Fire',
'artist': 'Low',
'tracks': [
'1: Sunflower',
'2: Whitetail',
'3: Dinosaur Act',
...
],
'widget': [
{
'id': '1234',
'name': 'my widget',
'description': 'my description'
}
]
}
I am trying:
class WidgetField(serializers.RelatedField):
def to_representation(self, value):
return {
'id': '1234'
....
}
class TrackListingField(serializers.RelatedField):
def to_representation(self, value):
...
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackListingField(many=True)
widget = WidgetField()
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
I keep getting the error:
AssertionError: Relational field must provide a `queryset` argument, override `get_queryset`, or set read_only=`True`.
Thanks
Ok, got it. It took a little bit of Googling / trial and error. Apparently you can override the function that returns data for the field.
For example:
class AlbumSerializer(serializers.ModelSerializer):
widget = serializers.SerializerMethodField()
def get_widget(self, data):
return {
'id': data.id
}
OR, you can do the following:
class WidgetSerializer(serializers.ModelSerializer):
class Meta:
model = Widget
fields = ('id', 'name', 'description',)
.... and in the AlbumSerializer.get_widget function:
def get_widget(self, data):
widget = Widget.objects.get(album=data.album, track=data.track)
return WidgetSerializer(widget, many=False, context=self.context).data
Finally, you don't have to use the function name "get_widget". You can name it whatever you want. Example:
class AlbumSerializer(serializers.ModelSerializer):
widget = serializers.SerializerMethodField("fn_override")
def fn_override(self, data):
....
You can follow the pattern in this SO question: Django REST Framework: adding additional field to ModelSerializer

Resources