Django REST framework: nested relationship: non_field_errors - python

In a Django-REST-framework project I tried to use the nested relationship and got a "non_field_errors" in the browsable API web view.
The code is from this part of the documentation: http://www.django-rest-framework.org/api-guide/relations#nested-relationships
models.py:
from django.db import models
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 Meta:
unique_together = ('album', 'order')
ordering = ('order',)
def __unicode__(self):
return '%d: %s' % (self.order, self.title)
serializers.py:
from rest_framework import serializers
from myapp.models import Album, Track
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = ('order', 'title')
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True)
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
ERROR (at ../albums):
The Track input field is marked red with the error message: non_field_errors.
Clicking the OPTIONS button reveals the actual&correct data structure:
Tracks nested with their appropriate propertie
The raw data input of the browsable browser view shows:
{
"album_name": "",
"artist": "",
"tracks": null
}
Posting some valid raw-data actually works. But it'd be nicer if the web interface form would work as well. Especially since I'm wondering if there's something funny going on anyway.
Thank you in advance!

I have experienced this issue as well. One way to get rid of the error is to use:
class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.RelatedField(many=True)
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
However, this removes the nested track fields and displays only a string representation of the tracks.
Edit: I figured it out. What you want is this:
class AlbumSerializer(serializers.ModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
read_only_fields = ('tracks',)
depth = 1
This will cause the tracks to nest without throwing the UI error.

One solution is to simply hide the HTML form on the browser side. For example, override Rest Framework's api.html template (by creating your_app/templates/rest_framework/api.html) and include the following:
{% extends "rest_framework/base.html" %}
...
{% block script %}
{{ block.super }}
<script>
$('.form-switcher a[name="html-tab"]').hide();
$('.form-switcher a[name="raw-tab"]').tab('show')
</script>
{% endblock %}
If you want to keep the HTML form for your flat endpoints and simply remove it from your nested ones, you could use the name variable as an indicator. For instance, include "Nested" in the names of your nested endpoints and do something like this:
if("{{ name }}".indexOf("Nested") >= 0){
$('.form-switcher a[name="html-tab"]').hide();
$('.form-switcher a[name="raw-tab"]').tab('show').hide();
}

Related

Joining Models in Python DRF Serializer without introducing Foreign key

So I am relatively new to Django, and DRF. I recently started a project to learn more about the platform.
I also found best practice guide line which I am following at the moment. Here is the Django Style Guide
The guild line says that every App should completely decouple itself and use UUID instead of foreign key,
And every business rule should go into services.py (Which I like btw.)
Here comes the problem. I am not sure how I can construct two models together to produce a nested JSON Output.
Ex: I have Post() model and Comments() model. and Comment model uses uuid of Post model as a referential id.
Now in my API.py, I am not sure how I can join these two together in the Serializer.py
FYI the code below is only for demo purposes, may not be executable
Model
class Post(models.Model):
user_id = models.UUIDField(default=uuid.uuid4)
title = models.CharField(max_length=100)
description = models.CharField(max_length=100)
pictures = models.CharField(max_length=100)
lat = models.CharField(max_length=16)
long = models.CharField(max_length=16)
vote = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now_add=True)
class Comment(models.Model):
post_id = models.UUIDField(default=uuid.uuid4)
parent_id = models.ForeignKey("self", on_delete=models.CASCADE)
text = models.CharField(max_length=1000)
up_vote = models.IntegerField()
down_vote = models.IntegerField()
user_id = models.UUIDField(default=uuid.uuid4)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now_add=True)
Service
def get_report(id) -> Dict:
logger.info('Get an Report by Id')
post= Post.objects.get(id=id)
return {
'post'= post
}
def get_comments(id) -> Dict:
logger.info('Get an Report by Id')
comments = Comment.objects.filter(post_id=id)
return {
'comments' = comments
}
API
class ReportGetApi(APIView):
class OutputSerializer(serializers.ModelSerializer):
comments = CommentsSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ('id', 'title', 'description', 'pictures', 'lat', 'long', 'vote', 'comments')
class CommentsSerializer(serializers.ModelSerializer):
class Meta:
model = Comments
fields = ('post_id', 'parent_id', 'text', 'up_vote', 'down_vote', 'user_id', 'created_at', 'modified_at')
def get(self, request):
post = PostService.get_post() #Only one item
comments = PostService.get_comments() #Many Items
serializer = self.OutputSerializer(post, many=True)
return Response(serializer.data)
You actually don't need a services file to do what you're asking. DRF can handle what you want directly with serializers and using a RetrieveAPIView.
So using your models you have above you could have something like the following:
serializers.py
from rest_framework import serializers
from . import models
class CommentSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True)
class Meta:
model = models.Comment
fields = '__all__'
class PostSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True)
comments = serializers.SerializerMethodField()
class Meta:
model = models.Post
fields = '__all__'
def get_comments(self, obj):
comments = models.Comment.objects.filter(post_id=obj.id)
return CommentSerializer(comments, many=True).data
views.py
from rest_framework import generics
from . import serializers, models
class ReportApi(generics.RetrieveAPIView):
serializer_class = serializers.PostSerializer
queryset = models.Post.objects.all()
You would need to specify the URL to pass in the primary key of the Post object you want to retrieve like so:
urls.py
from django.urls import path
from api import views
app_name = 'api'
urlpatterns = [
path('get_post_report/<pk>/', views.ReportApi.as_view()),
]
Then you would access the view using something like http://example.com/api/get_post_report/12345678/.
Note: You must configure the urls.py within your project's
urls file to use 'api/' for including your app's urls for the
'/api/' part of the url above to be a part of the url.
If you don't know how to set up urls refer to the Django Tutorial
This will then give you something like the following:
response.json
{
"id": 1,
"comments": [
{
"id": 1,
"post_id": "00000000-0000-0000-0000-000000000001",
"text": "Comment Text",
"up_vote": 0,
"down_vote": 0,
"user_id": "00000000-0000-0000-0000-000000000001",
"created_at": "2020-05-29T13:14:07.103072Z",
"modified_at": "2020-05-29T13:14:07.103124Z",
"parent_id": null
}
],
"user_id": "00000000-0000-0000-0000-000000000001",
"title": "My Post",
"description": "Post Description",
"pictures": "No Pictures",
"lat": "12",
"long": "12",
"vote": "12",
"created_at": "2020-05-29T13:14:07.102316Z",
"modified_at": "2020-05-29T13:14:07.102356Z"
}
Lastly
I looked at the guide you are referring to and did not see a reference to decouple models with UUID's. While you can do that (and in some cases may be necessary) I would think hard on whether you truly need that much decoupling.
There are reasons to use foreign keys over UUID's such as accessing the related models easier and faster. Decoupling using UUID's means you will need to write more boilerplate code every time you need to access a related model.
It comes down to deciding whether you need decoupling or a better developer experience with (subjectively) cleaner code. Please don't just follow a guide and assume it's law. More experience will help with this.
For example, if you used a foreign key relationship your PostSerializer could look like the following:
example_serializers.py
class PostSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True)
comments = CommentSerializer(many=True)
class Meta:
model = models.Post
fields = '__all__'
See how we got rid of the get_comments(self, obj) method? That was just one bit of boilerplate that we got rid of by making the design decision to use foreign key relationships. Now just imagine a codebase of millions of lines of code and many serializers. Remember, the more code you write the more testing/debugging you need to do as well.
Again, just my opinion, but be sure you actually need to decouple your models before you do it.
Also, I strongly recommend you follow the DRF tutorial. It reviews everything you need to accomplish what I just posted here.
Hope this helps!
You could create one serializer called OutputSerializer and include the comments as its own field? if you change comments = OutputCommentSerializer to comments = CommentsSerializer(many=True, read_only=True) I believe that will depict what you want...(example below). You already defined CommentSerializer, thus you can use it.
class ReportGetApi(APIView):
class OutputSerializer(serializers.ModelSerializer):
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ('id', 'title', 'description', 'pictures', 'lat', 'long', 'vote', 'comments')
class CommentsSerializer(serializers.ModelSerializer):
class Meta:
model = Comments
fields = ('post_id', 'parent_id', 'text', 'up_vote', 'down_vote', 'user_id', 'created_at', 'modified_at')
def get(self, request):
post = PostService.get_post() #Only one item
comments = PostService.get_comments() #Many Items
serializer = self.OutputSerializer(post, many=True)
return Response(serializer.data)

how to display information entered by a user into a django model in the User profile

I'm kind of new to django, I'm working on a project currently. It is a website where people can look for houses to rent. Users will be able to create accounts, search for houses to rent and create listings about the houses they want to rent out.
I created a model to save all the information about houses that users want to rent out. I need to filter this information and display each user's listing on their profile. I have searched online but no solution yet.
Really need help.
models.py
from django.db import models
from django.contrib.auth.models import User
class Myhouses(models.Model):
Available = 'A'
Not_Available = 'NA'
Availability = (
(Available, 'Available'),
(Not_Available, 'Not_Available'),
)
name_of_accomodation = models.CharField(max_length=200)
type_of_room = models.CharField(max_length=200)
house_rent = models.IntegerField()
availability = models.CharField(max_length=2, choices=Availability, default=Available,)
location = models.CharField(max_length=200)
nearest_institution = models.CharField(max_length=200)
description = models.TextField(blank=True)
image = models.ImageField(upload_to='profile_image')
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='author')
def __str__(self):
return self.name_of_accomodation
view.py
class ListingByUser(LoginRequiredMixin, generic.ListView):
model = Myhouses
template_name ='houses/ListingByUser.html'
paginate_by = 10
def get_queryset(self):
return Myhouses.objects.filter(author=self.request.user)
urls.py
from django.conf.urls import url, include
from . import views
from django.contrib.auth.models import User
urlpatterns = [
url(r'^addlisting/$', views.addlisting, name='addlisting'),
url(r'^mylisting/', views.ListingByUser.as_view(), name='ListingByUser')
]
Template
<ul>
{% for houses in myhouses_list %}
<li>{{ houses.name_of_accomodation }}</li>
{%endfor %}
</ul>
Taking a quick view of your code, there is something that stuck me on your ListingByUser view: you override the get method only to set some attributes that are normaly defined as class attributes. That also could be preventing your view to actually get your models out of the database (via calling the get_queryset method) and rendering a proper response.
Edit
I found there's also a problem linking your template to the response the ListingByUser view is rendering. As far as I know, Django views doesn't look into the variable template_name for getting the response's template. But it does call a method get_template_names which returns a list of template names given as strings.
Try to modify it in this way:
views.py
class ListingByUser(LoginRequiredMixin, generic.ListView):
model = Myhouses
template_name ='myhouses/listing_by_user.html'
paginate_by = 10
def get_queryset(self):
return Myhouses.objects.filter(author=self.request.user)
def get_template_names(self):
return [self.template_name]

Multiple models in one form

I want to use two models in one form. Models have many to many relation, what would be the nicest solution for that? I tried formset, but I saw only one model's fields, not both.
My model:
class Event(models.Model):
title = models.CharField(max_length=100)
description = models.TextField(max_length=400)
location = models.CharField(max_length=100)
class EventTime(models.Model):
start_time = models.DateTimeField()
event = models.ManyToManyField(Event)
Define a model form for each form.
class EventForm(forms.ModelForm):
class Meta:
fields = ('title', 'description', 'location')
class EventTimeForm(forms.ModelForm):
class Meta:
fields = ('start_time',)
Note that the event field has been left out, because we want to link the event time object to the new event we are creating.
In your view, you need to pass both forms to the template context. Using prefix is a good idea when using multiple forms, it prevents field names from clashing in request.POST
event_form = EventForm(prefix="event")
event_time_form = EventTimeForm(prefix="eventtime")
In the template, you can include multiple forms in the same form tag.
<form method="post">
{{ event_form }}
{{ event_time_form }}
</form>
When processing the POST request, check that both forms are valid. After saving both forms, you can link the event and event time together.
event_form = EventForm(request.POST, prefix="event")
event_time_form = EventTimeForm(request.POST, prefix="eventtime")
if event_form.is_valid() and event_time_form.is_valid():
event = event_form.save()
event_time = event_time_form.save()
event_time.event.add(event)

Widget error with TinyMCE in Django

I'm getting this error with TinyMCE in django:
TypeError: init() got an unexpected keyword argument 'widget'
I have followed the instructions as I found them, and don't know why the error is there. Here is the model:
class Article(models.Model):
"""Represents a wiki article"""
title = models.CharField(max_length=100)
slug = models.SlugField(max_length=50, unique=True)
text = models.TextField(widget=TinyMCE(attrs={'cols': 80, 'rows': 30}))# (help_text="Formatted using ReST")
author = models.ForeignKey(User)
is_published = models.BooleanField(default=False, verbose_name="Publish?")
created_on = models.DateTimeField(auto_now_add=True)
objects = models.Manager()
published = PublishedArticlesManager()
The comment "#formatted in ReST" is because the original TextField was using restructuredText. I was able to actually get tinyMCE from CDN and place a very simpel script in the head of the appropriate template. It loaded the wysiwyg editor, but then rendered the saved page with HTML tags visible.
So I added to form declaration as:
from django import forms
from models import Article, Edit, FileUploadHandler
from tinymce import models as tinymce_models
class ArticleForm(forms.ModelForm):
class Meta:
text = forms.Charfield(widget=TinyMCE(attrs={'cols': 80, 'rows': 30}))#(help_text="Formatted using ReST")
model = Article
exclude = ['author', 'slug']
class EditForm(forms.ModelForm):
class Meta:
model = Edit
fields = ['summary']
class UploadImageForm(forms.ModelForm):
class Meta:
model = FileUploadHandler
image = forms.ImageField()
fields = ['title']
The editor is there, but upon save its rendering the article with html tags visible. Why is that?
widget is an attribute for form fields, not model fields. You need to move that setting to your form declaration (or formfield_overrides if you're trying to use it in the admin).
To display the marked up content without escaping the HTML tags in a later view, one way is to use the |safe built in filter.

Is it possible to sort elements in a ToManyField attribute using TastyPie?

I have a REST API using Django Tastypie. Given the following code
The models
class BlogPost(models.Model):
# class body omitted, it has a content and an author
class Comment(models.Model):
blog_post = models.ForeignKey(BlogPost, related_name="comments")
published = models.DateTimeField()
# rest of class omitted
The resources
class CommentResource:
# omitted
class BlogPostResource(ModelResource):
comments = fields.ToManyField("resources.CommentResource",
attribute="comments")
When I ask for a blogpost I get something like:
GET: api/blogpost/4/
{
'content' : "....",
'author' : "....",
'comments' : ['api/comment/4/', 'api/comment/5']
}
However, the comments are not necessarily sorted by any field. I would like to make sure they are sorted by a specific key (published)
Is there any way to achieve this?
I managed to solve the issue by changing the field in BlogPostResource to the following:
class BlogPostResource(ModelResource):
comments = fields.ToManyField("resources.CommentResource",
attribute=lambda bundle: bundle.obj.comments.all().order_by("published"))
You could also try adding an ordering in the actual Comment Model (not in the tastypie Comment ModelResource):
class Comment(models.Model):
blog_post = models.ForeignKey(BlogPost, related_name="comments")
published = models.DateTimeField()
class Meta:
ordering = ['published']

Categories

Resources