im trying to add a custom action to my ViewSet in Django2, using django-rest-framework. Problem is that my serializer is not serializing nested model and thus giving me error:
{
"labels": [
{
"non_field_errors": [
"Invalid data. Expected a dictionary, but got Label."
]
},
{
"non_field_errors": [
"Invalid data. Expected a dictionary, but got Label."
]
}
]
}
I have two models which have M:N relationship.
Label model:
class Label(models.Model):
name = models.CharField(max_length=30, help_text='Name of Label')
desc = models.CharField(max_length=200, help_text='Description of Label')
def __str__(self):
return self.name
LabelSet model:
class LabelSet(models.Model):
labels = models.ManyToManyField(Label, blank=True, help_text='ManyToMany field of corresponding labels')
name = models.CharField(max_length=30, help_text='Name of Label Set')
desc = models.CharField(max_length=200, help_text='Description of Label Set')
def __str__(self):
return self.name
Machine Model:
class Machine(models.Model):
name = models.CharField(max_length=30, help_text='Name of machine')
desc = models.CharField(max_length=200, help_text='Description of machine')
location = models.ForeignKey(Location, null=True, blank=True, on_delete=models.CASCADE, help_text='ID of machine location')
labelset = models.ForeignKey(LabelSet, null=True, blank=True, on_delete=models.DO_NOTHING, help_text='ID of set of labels relevant for this machine')
def __str__(self):
return self.name
Serializers:
class LabelSerializer(serializers.ModelSerializer):
class Meta:
model = Label
fields = '__all__'
class LabelSetSerializer(serializers.ModelSerializer):
qs = Label.objects.all().values()
labels = LabelSerializer(qs, many=True)
class Meta:
depth = 1
model = LabelSet
fields = ('name', 'desc', 'labels')
Custom action in viewsets.py (I want to retrieve available labels by machine, so path is /machines/{id}/labels
class MachineViewSet(viewsets.ModelViewSet):
'''
A viewset used for retrieving and editing Machine instances.
'''
#permission_classes = (DRYPermissions,)
serializer_class = MachineSerializer
queryset = Machine.objects.all()
# /api/v1/machines/{machine_id}/labels
#action(detail=True)
def labels(self, request, pk=None):
# Get labelset id
ls = Machine.objects.get(pk=pk).labelset
# Get LabelSet instance
serializer = LabelSetSerializer(data=model_to_dict(ls))
if serializer.is_valid():
return Response(serializer.data)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
The endpoint works fine, but when querying /machines/1/labels i got the response which is the first snippet:
"Invalid data. Expected a dictionary, but got Label."
Im literally out of ideas, tried even making dict from qs = Label.objects.all().values() in Serializer, no luck.
Thanks to #Jerin Peter George, output is now:
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"name": "TestSet",
"desc": "asd",
"labels": [
{
"id": 1,
"name": "OK",
"desc": "desc"
},
{
"id": 2,
"name": "Broken",
"desc": "asd"
}
]
}
So /api/v1/machines/1/labels works, but suddenly /api/v1/machines/ does not. (502 Bad Gateway with error TypeError: 'LabelSet' object is not iterable)
APP level urls:
from django.conf.urls import url
from devices.viewsets import *
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'devices', DeviceViewSet, base_name='device')
router.register(r'projects', ProjectViewSet, base_name='project')
router.register(r'locations', LocationViewSet, base_name='location')
router.register(r'industries', IndustryViewSet, base_name='industry')
router.register(r'companies', CompanyViewSet, base_name='companies')
router.register(r'project_types', ProjectTypeViewSet, base_name='project_type')
router.register(r'device_types', DeviceTypeViewSet, base_name='device_type')
router.register(r'machines', MachineViewSet, base_name='machine')
router.register(r'records', RecordViewSet, base_name='record')
router.register(r'labels', LabelViewSet, base_name='label')
router.register(r'labelsets', LabelSetViewSet, base_name='label_set')
urlpatterns = router.urls
App level urls.py
from django.contrib import admin
from django.conf.urls import url
from django.urls import include, path
from rest_framework.documentation import include_docs_urls
from rest_framework_expiring_authtoken import views
from devices.views import AudioUploadView
API_PREFIX = 'api/v1/'
urlpatterns = [
url(API_PREFIX + 'docs/', include_docs_urls(title='API Docs')),
url(API_PREFIX + 'admin/', admin.site.urls),
url(API_PREFIX + 'api-token-auth/', views.obtain_expiring_auth_token),
path(API_PREFIX, include('devices.urls'))
]
EDIT: SOLVED
Apparently i added one more nested serializer to MachineSerializer
class MachineSerializer(serializers.ModelSerializer):
labelsets = LabelSetSerializer(many=True)
class Meta:
model = Machine
fields = '__all__'
So removing the line labelsets = LabelSetSerializer(many=True) did the trick.
And that is where the error came from, now is everything working as expected, thanks:)
Replace your labels() with below snippet,
#action(detail=True)
def labels(self, request, pk=None):
# Get labelset id
ls = Machine.objects.get(pk=pk).labelset
# Get LabelSet instance
serializer = LabelSetSerializer(ls)
return Response(serializer.data)
Related
Hope you all are doing well.
Is there any way to add the full URL in embed image src in API?
Here is the API sample
"content": [
{
"type": "full_richtext",
"value": "<p data-block-key=\"11dr5\"> Example: The database consists of information about a set of</p><p data-block-key=\"4l7vp\"></p><img alt=\"image-325682-1594637051\" class=\"richtext-image full-width\" height=\"410\" src=\"/media/images/image-325682-1594637051.width-800.jpg\" width=\"728\"><p data-block-key=\"b41bt\"></p><p data-block-key=\"eorjk\"> customers and accounts and the relationship between them) </p>",
"id": "f21e7928-f81c-477a-ab42-ba3bc2cd9226"
}
]
how I can add this type of URL like in that src=\"example.com/media/images/imagename\"?
Here is my models.py file.
"""Blog listing and blog detail pages."""
from django.db import models
from modelcluster.fields import ParentalManyToManyField
from django import forms
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel, MultiFieldPanel, InlinePanel
from wagtail.core.fields import StreamField
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.core.models import Page
from wagtail.snippets.models import register_snippet
# from . import blocks
from . import blocks
from .blocks import InlineVideoBlock
from rest_framework import fields, serializers
from modelcluster.fields import ParentalKey
from wagtail.api import APIField
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase
from django.shortcuts import render
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
#added by fathi
from rest_framework.fields import Field
#register_snippet
class BlogCategory(models.Model):
"""Blog category for a snippet."""
name = models.CharField(max_length=255)
slug = models.SlugField(
verbose_name="slug",
allow_unicode=True,
max_length=255,
help_text='A slug to identify posts by this category',
)
panels = [
FieldPanel("name"),
FieldPanel("slug"),
]
class Meta:
verbose_name = "Blog Category"
verbose_name_plural = "categories"
ordering = ["name"]
def __str__(self):
return self.name
class CreateNewPage(RoutablePageMixin, Page):
"""Listing page lists all the Blog Detail Pages."""
# template = "blog/create_new_page.html"
custom_title = models.CharField(
max_length=100,
blank=False,
null=False,
help_text='Overwrites the default title',
)
content_panels = Page.content_panels + [
FieldPanel("custom_title"),
]
def get_context(self, request, *args, **kwargs):
"""Adding custom stuff to our context."""
context = super().get_context(request, *args, **kwargs)
context["posts"] = AddStory.objects.live().public()
context["categories"] = BlogCategory.objects.all()
all_posts = AddStory.objects.live().public().order_by('-first_published_at')
if request.GET.get('tag', None):
tags = request.GET.get('tag')
all_posts = all_posts.filter(tags__slug__in=[tags])
context["posts"] = all_posts
return context
#route(r"^category/(?P<cat_slug>[-\w]*)/$", name="category_view")
def category_view(self, request, cat_slug):
"""Find blog posts based on a category."""
context = self.get_context(request)
try:
# Look for the blog category by its slug.
category = BlogCategory.objects.get(slug=cat_slug)
except Exception:
# Blog category doesnt exist (ie /blog/category/missing-category/)
# Redirect to self.url, return a 404.. that's up to you!
category = None
if category is None:
# This is an additional check.
# If the category is None, do something. Maybe default to a particular category.
# Or redirect the user to /blog/ ¯\_(ツ)_/¯
pass
context["posts"] = AddStory.objects.live().public().filter(categories__in=[category])
# Note: The below template (latest_posts.html) will need to be adjusted
return render(request, "blog/latest_posts.html", context)
#route(r'^latest/?$', name="latest_posts")
def latest_blog_posts_only_shows_last_5(self, request, *args, **kwargs):
context = self.get_context(request, *args, **kwargs)
context["posts"] = context["posts"][:10]
return render(request, "blog/latest_posts.html", context)
class BlogPageTag(TaggedItemBase):
content_object = ParentalKey(
'AddStory',
related_name='tagged_items',
on_delete=models.CASCADE
)
class AddStory(Page):
"""Blog detail page."""
custom_title = models.CharField(
max_length=100,
blank=False,
null=False,
help_text='Overwrites the default title',
)
blog_image = models.ForeignKey(
"wagtailimages.Image",
blank=False,
null=True,
related_name="+",
on_delete=models.SET_NULL,
)
categories = ParentalManyToManyField("blog.BlogCategory", blank=True)
content = StreamField(
[
("full_richtext", blocks.RichtextBlock()),
("simple_richtext", blocks.SimpleRichtextBlock()),
("Add_video", blocks.VideoRichtextBlock()),
('video', InlineVideoBlock()),
],
null=True,
blank=True,
)
#exposing custom field in API
api_fields = [
# APIField("blog_authors"),
# Exposed StreamFields
APIField("blog_image"),
APIField("content"),
APIField("categories", serializer=serializers.ListSerializer(child=fields.CharField())),
APIField("tags"),
]
content2 = StreamField(
[
("full_richtext", blocks.RichtextBlock()),
],
null=True,
blank=True,
)
tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
content_panels = Page.content_panels + [
FieldPanel("custom_title"),
ImageChooserPanel("blog_image"),
StreamFieldPanel("content"),
FieldPanel('tags'),
MultiFieldPanel(
[
FieldPanel("categories", widget=forms.CheckboxSelectMultiple),
],
heading="Categories"
),
]
class VideoBlogPage(AddStory):
"""A video subclassed page."""
template = "blog/video_blog_page.html"
youtube_video_id = models.CharField(max_length=30)
content_panels = Page.content_panels + [
FieldPanel("custom_title"),
ImageChooserPanel("blog_image"),
# MultiFieldPanel(
# [
# InlinePanel("blog_authors", label="Author", min_num=1, max_num=4)
# ],
# heading="Author(s)"
# ),
MultiFieldPanel(
[
FieldPanel("categories", widget=forms.CheckboxSelectMultiple)
],
heading="Categories"
),
FieldPanel("youtube_video_id"),
StreamFieldPanel("content2"),
]
I have searched many places and looked this tutorial but it didn't worked. https://learnwagtail.com/tutorials/headless-cms-serializing-richtext-blocks/
You can add a serializer. Then in the APIField add this as your serializer.
from rest_framework.fields import Field
class ImageSerializerField(Field):
def to_representation(self, value):
return {
"url": value.file.url,
"title": value.title,
"width": value.width,
"height": value.height,
}
I have a django-application with the followin files
models.py
from datetime import datetime
class Comment(object):
def __init__(self, email, content, created=None):
self.email = email
self.content = content
self.created = created or datetime.now()
serializers.py
from rest_framework import serializers
class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()
url = serializers.CharField(source='get_absolute_url', read_only=True)
in views.py I now define a a ViewSet to return the serialized results. In this class I define a list of comments
views.py
from rest_framework import viewsets
from .serializers import *
from .models import Comment
from rest_framework.response import Response
class CommentViewSet(viewsets.ViewSet):
lc = [Comment(email='jan#auto.com', content='hallo mike'),
Comment(email='jan#auto.com', content='hallo jan'),
Comment(email='jan#auto.com', content='hallo niklas')]
def list(self, request):
serializer = CommentSerializer(self.lc, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
user = self.lc[int(pk)]
serializer = CommentSerializer(user)
return Response(serializer.data)
When I now call the api (http://127.0.0.1:8000/comments/?format=json) I get the following result
[
{
"email": "jan#auto.com",
"content": "hallo mike",
"created": "2019-08-16T16:53:56.371890Z"
},
{
"email": "jan#auto.com",
"content": "hallo jan",
"created": "2019-08-16T16:53:56.371890Z"
},
{
"email": "jan#auto.com",
"content": "hallo niklas",
"created": "2019-08-16T16:53:56.371890Z"
}
]
In this response I would have hoped to see a url for each dataset. The error ist probably thats for url = serializers.CharField(source='get_absolute_url', read_only=True) the source is undefined in the Comment class. However I have no clue how to achieve this. Any help is appreciated.
You need to define a get_absolute_url method [Django-doc] on your model, like:
# app/models.py
from django.db import models
from django.urls import reverse
class Comment(models.Model):
email = models.EmailField()
content = models.CharField(max_length=128)
created = models.DateTimeField(auto_now_add=True)
def get_absolute_url(self):
return reverse('name-of-view', kwargs={'pk': self.pk})
Here 'name-of-view' is the name of the view you defined, for example in your urls.py, and the kwargs=... is given a dictionary that maps the values for the corresponding URL path parameters.
I created a serializer to post a results, but when I try to use postman it says that the value 'enrollment_id' is null:
views.py
from rest_framework import generics
from .serializers import ResponseSerializer
class ResponseCreate(generics.CreateAPIView):
serializer_class = ResponseSerializer
serializers.py
class ResponseSerializer(serializers.ModelSerializer):
class Meta:
model = Response
fields = (
'enrollment_id',
'evaluation_id',
'question_id',
'question_component_id',
'user_id',
)
urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.ResponseCreate.as_view()),
]
In the postman post body I send the following json:
{
"enrollment_id": 1,
"user_id": 2,
"question_component_id": 2,
"question_id": 1,
"evaluation_id": 1
}
Error postman:
IntegrityError at /response/
null value in column "enrollment_id" violates not-null constraint
DETAIL: Failing row contains (9, null, null, null, null, null).
view
Edit:
Model Response:
class Response(models.Model):
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
)
enrollment = models.ForeignKey(
Enrollment,
on_delete=models.CASCADE,
)
evaluation = models.ForeignKey(
Evaluation,
on_delete=models.CASCADE,
)
question = models.ForeignKey(
Question,
on_delete=models.CASCADE,
)
question_component = models.ForeignKey(
Question_Component,
on_delete=models.CASCADE,
)
Edit:
Your serializer need not add '_id' after the Model fields.
It should be like this:
class ResponseSerializer(serializers.ModelSerializer):
class Meta:
model = Response
fields = (
'enrollment',
'evaluation',
'question',
'question_component',
'user',
)
Now try seding the modified JSON via Postmen:
{
"enrollment": 1,
"user": 2,
"question_component": 2,
"question": 1,
"evaluation": 1
}
You need to define a queryset in your view:
class ResponseCreate(generics.CreateAPIView):
queryset = Response.objects.all()
serializer_class = ResponseSerializer
You have to indicate in your url patter the methods that are going to be allowed:
urlpatterns = [
path('', views.ResponseCreate.as_view({'get': 'list'})),
]
I can see in the error you get that `GET method is not allowed, that's because you didn't indicate Django to allow it.
Try this view:
from rest_framework import viewsets
from .serializers import ResponseSerializer
class ResponseCreate(viewsets.ModelViewSet):
queryset = models.Response.objects.all()
serializer_class = serializers.ResponseSerializer
ModelViewSet has already the proper response for al methods, but you have to indicate in your url pattern which ones are allowed.
Let's say I have three models as:
class User(AppModel):
name = models.CharField(max_length=255)
class Business(AppModel):
owner = models.ForeignKey("User", related_name="businesses", on_delete=models.CASCADE)
legal_name = models.CharField(max_length=255)
class Invoice(AppModel):
business = models.ForeignKey("Business", related_name="invoices", on_delete=models.CASCADE)
amount = models.integerField()
As you can see, a user can have multiple businesses and a business can have multiple invoices.
My serializers.py:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields= ('name')
class BusinessSerializer(serializers.ModelSerializer):
owner = UserSerializer(many=False)
class Meta:
model = Business
fields= ('owner','legal_name')
class InvoiceSerializer(serializers.ModelSerializer):
business= BusinessSerializer(many=False)
class Meta:
model = Invoice
fields= ('business','amount')
views.py:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class BusinessViewSet(viewsets.ModelViewSet):
queryset = Business.objects.all()
serializer_class = BusinessSerializer
class InvoiceViewSet(viewsets.ModelViewSet):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer
urls.py:
router = DefaultRouter()
router.register('user', UserViewSet, base_name='users')
router.register('business', BusinessViewSet, base_name='businesses')
router.register('invoice', InvoiceViewSet, base_name='invoices')
urlpatterns = router.urls
http://example.com/api/user returns all users. Not a problem.
But the functionality I'm looking for is:
http://example.com/api/business/ returns
[
{
"legal_name": "1business",
"owner": 1,
},
{
"legal_name": "2business",
"owner": 1,
},]
http://example.com/api/business/1/ returns
{
"legal_name": "1business",
"owner": 1,
}
The above is ok. But I also need:
http://example.com/api/business/1/invoices/ should return
[
{
"business": 1,
"amount": 100,
},
{
"business": 1,
"amount": 999,
},]
As well I should be able to create update delete those invoices there.
Any Help? I'm new to django rest framework. The above classes are just a sample. Ignore errors.
You should use django decorators which are #list_route and #detail_route for your viewset. But be careful with your DRF version. Because those decorators merged together as #action in DRF 3.8+. Here is the announcement.
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
class BusinessViewSet(viewsets.ModelViewSet):
queryset = Business.objects.all()
serializer_class = BusinessSerializer
#action(detail=True, methods=["GET"], url_path="invoices")
def invoices(self, request, pk=None):
"""
Your codes comes here to return related result.
pk variable contains the param value from url.
if you do not specify the url_path properties then action will accept the function's name as url path.
"""
entity = Invoice.objects.filter(business=pk)
serializer = self.get_serializer(entity, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
Then, you will be able to call this endpoints from;
http://example.com/api/business/{{PK}}/invoices/
http://example.com/api/business/1/invoices/
http://example.com/api/business/3/invoices/
http://example.com/api/business/23/invoices/
Here you can find more details about #actions from documentation.
PS: Don't forget to control empty entity results in your codes. You should return correct response with correct status codes.
I try to send the following data to my django application:
{
"hashtags": ["hashtag"],
"title": "title",
"message": "message"
}
and i get this response:
{
"hashtags": [
{
"non_field_errors": [
"Invalid data. Expected a dictionary, but got int."
]
}
]
}
I have the following view defined in views.py
class PostList(generics.ListCreateAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = IsAuthorizedOwnerOrReadOnly,
the models are defined like this:
class Post(models.Model):
ambassador = models.OneToOneField("User")
publish_date = models.DateTimeField(auto_now_add=True, null=True, blank=True)
hashtags = models.ManyToManyField("PostHashtags", related_query_name="posts")
title = models.CharField(max_length=TEXT_SHORT, null=True, blank=True)
message = models.CharField(max_length=TEXT_MIDDLE, null=True, blank=True)
class PostHashtags(models.Model):
hashtag = models.CharField(max_length=TEXT_SHORT, null=False)
def __unicode__(self):
return self.hashtag
and i define the serializers like this:
class PostHashtagSerializer(serializers.ModelSerializer):
class Meta:
model = PostHashtags
fields = ("hashtag",)
class PostSerializer(serializers.ModelSerializer):
hashtags = PostHashtagSerializer(many=True)
class Meta:
model = Post
fields = ("id", "hashtags", "title", "message",)
read_only_fields = ("id", 'account_name',)
It seems like the hashtags are not created automatically using my current serialisation config. Is there a way to have my hashtags created if they do not yet exist, and if they do exist, have the Post use that same hashtag? In that case, how should my serialisers be defined?
EDIT 1:
After GwynBleidD's suggestions I now get the following error:
The `.create()` method does not support writable nestedfields by default.
Write an explicit `.create()` method for serializer PostSerializer , or set `read_only=True` on nested serializer fields.
Does anyone have a suggestion for such a create method?
EDIT 2: solved it using the following serialisers
class PostHashtagSerializer(serializers.ModelSerializer):
hashtag = serializers.CharField()
class Meta:
model = PostHashtags
fields = ("hashtag",)
class PostSerializer(serializers.ModelSerializer):
hashtags = PostHashtagSerializer(many=True,)
class Meta:
model = Post
fields = ("ambassador", "hashtags", "title", "message",)
def create(self, validated_data):
hashtags_data = validated_data.pop('hashtags')
post = Post.objects.create(**validated_data)
for hashtag in hashtags_data:
ht = PostHashtags.objects.create()
ht.hashtag = hashtag.get("hashtag")
ht.save()
post.hashtags.add(ht)
post.save()
return post
Hashtags are not string, but dict in that example. You have to submit:
{
"hashtags": [{"hashtag": "hashtag"}],
"title": "title",
"message": "message"
}