django rest framework nested relationships - - python

I have some trouble understanding Django Rest Framework. I am building an application, very simple. And for some reason I don't understand I am not able to make it work properly.
I am using:
python: 3.4.0
Django: 1.7.4
Django Rest Framework: 3.0.5
Here is the thing:
#models.py
class Country(StandardMetadata, GeoBase):
CONTINENT_CHOICES = (
('OC', 'Oceania'),
('EU', 'Europe'),
('AF', 'Africa'),
('NA', 'North America'),
('AN', 'Antarctica'),
('SA', 'South America'),
('AS', 'Asia'),
)
name = models.CharField(max_length=200, db_index=True)
slug = AutoSlugField(populate_from='name')
continent = models.CharField(max_length=2, choices=CONTINENT_CHOICES, default='NA')
iso2 = models.CharField(max_length=2)
iso3 = models.CharField(max_length=3)
def __str__(self):
return self.name
class Meta:
verbose_name_plural = 'Countries'
class Region(StandardMetadata, GeoBase):
name = models.CharField(max_length=200, db_index=True)
slug = AutoSlugField(populate_from='name')
iso2 = models.CharField(max_length=2)
country = models.ForeignKey(Country)
def __str__(self):
return self.get_full_name()
def get_full_name(self):
return ', '.join([self.name, self.country.name])
class Meta:
unique_together = ('slug', 'country')
verbose_name = 'Region/State/Province'
verbose_name_plural = 'Regions/States/Provinces'
#serializers.py
class CountrySerializer(serializers.ModelSerializer):
class Meta:
model = Country
fields = ('id', 'name', 'slug', 'iso2', 'iso3', 'continent', 'lng', 'lat')
class RegionSerializer(serializers.ModelSerializer):
country = CountrySerializer(read_only=True, required=True, many=False)
class Meta:
model = Region
fields = ('id', 'name', 'slug', 'iso2', 'lng', 'lat', 'country',)
#views.py
class CountryViewSet(viewsets.ModelViewSet):
queryset = Country.objects.all()
serializer_class = CountrySerializer
class RegionViewSet(viewsets.ModelViewSet):
queryset = Region.objects.all()
serializer_class = RegionSerializer
#urls.py
router = routers.DefaultRouter()
router.register(r'countries', CountryViewSet)
router.register(r'regions', RegionViewSet)
So for some reason I got this error when I try to access to this url ( http://192.168.33.30:5000/regions/ ): May not set both read_only and required
What am I missing ?
Thanks a lot for your help !
EDIT
I found one of my problem. I removed the line unique_together = ('slug', 'country') in my Region model. I set the slug field as unique. The second problem is that I had to break my model to make it work.

Rather than specify read-only in the Serializer, do this:
class RegionSerializer(serializers.ModelSerializer):
country = CountrySerializer(required=True, many=False)
class Meta:
model = Region
fields = ('id', 'name', 'slug', 'iso2', 'lng', 'lat', 'country',)
read_only_fields = ('country', )

Related

DRF Add annotated field to nested serializer

I have two serializers that represent comments and their nested comments. I'm provide a queryset to viewset with annotated field likes. But my problem is that field only working in parent serializer. When i add this field to nested serializer i got error
Got AttributeError when attempting to get a value for field likes on serializer CommentChildrenSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the Comment instance.
Original exception text was: 'Comment' object has no attribute 'likes'.
Here is some my code. Thanks
Models.py
class Post(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
slug = models.SlugField(blank=True)
body = models.TextField()
tags = TaggableManager(blank=True)
pub_date = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-pub_date']
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE,
related_name='comments')
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
text = models.TextField(max_length=500)
pub_date = models.DateTimeField(auto_now=True)
parent = models.ForeignKey('self', blank=True, null=True,
on_delete=models.CASCADE, related_name='children')
class Meta:
ordering = ['-pub_date']
class Vote(models.Model):
comment = models.ForeignKey(Comment, on_delete=models.CASCADE,
related_name='votes')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
choice = models.BooleanField(null=True)
Serializers.py
class PostRetrieveSerializer(PostSerializer):
comments = CommentSerializer(read_only=True, many=True)
author = AuthorInfoSerializer(serializers.ModelSerializer)
class Meta:
model = Post
fields = ['id', 'author', 'slug', 'title', 'body', 'tags', 'pub_date', 'comments']
class CommentChildrenSerializer(serializers.ModelSerializer):
author = AuthorInfoSerializer(read_only=True)
likes = serializers.IntegerField()
class Meta:
model = Comment
fields = ['author', 'id', 'text', 'pub_date', 'parent', 'likes']
class CommentSerializer(serializers.ModelSerializer):
author = AuthorInfoSerializer(read_only=True)
children = CommentChildrenSerializer(many=True)
likes = serializers.IntegerField()
class Meta:
ordering = ['pub_date']
model = Comment
fields = ['author', 'id', 'text', 'pub_date', 'children', 'likes']
Views.py
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all().prefetch_related(
Prefetch('comments', queryset=Comment.objects.filter(parent__isnull=True)
.annotate(likes=Count('votes__choice'))))
serializer_class = PostSerializer
permission_classes = [IsOwnerOrAdminOrReadOnly]
pagination_class = PostPagination
lookup_field = 'slug'
def get_serializer_class(self):
""""
Attach related comments
when get post detail
"""
if self.action == 'retrieve':
return PostRetrieveSerializer
return self.serializer_class
def perform_create(self, serializer):
serializer.save(author=self.request.user)
maybe you can do something like this, adding the like field in each child comment.
queryset = Post.objects.all()\
.prefetch_related(
Prefetch(
'comments',
queryset=Comment.objects\
.filter(parent__isnull=True)\
.annotate(likes=Count('votes__choice'))\
.prefetch_related(
'children',
queryset=Comments.objects.all()\
.annotate(likes=Count('votes__choice'))
)
)
)
I hope this help you.
Regards!
On your model level, add a custom property like the below.
class Comment(models.Model):
...
class Meta:
ordering = ['-pub_date']
#property
def likes(self):
return self.votes.count()
On your serializer add SerializerMethodField
class CommentChildrenSerializer(serializers.ModelSerializer):
author = AuthorInfoSerializer(read_only=True)
likes = serializers.SerializerMethodField() # Change here
class Meta:
model = Comment
fields = ['author', 'id', 'text', 'pub_date', 'parent', 'likes']
# method for the SerializerMethodField
def get_likes(self, obj):
return obj.likes
Update both Comment related serializers. I believe this approach is simpler than the current implementation.

ManyToManyField does not show

Want to use REST API to populate my tables but my field does not display on the API page.
Models (Series, Roster):
class Series(models.Model):
(...)
def __str__(self):
return self.title
class Roster(models.Model):
(...)
series = models.ManyToManyField(Series)
(...)
def __str__(self):
return self.name
Serializers:
class SeriesSerializer(serializers.ModelSerializer):
class Meta:
model = Series
fields = ('id', 'title', 'icon')
read_only_fields = ('slug',)
class RosterSerializer(serializers.ModelSerializer):
series = SeriesSerializer(many=True, read_only=True)
class Meta:
model = Roster
fields = ('id', 'name', 'number', 'primary_color', 'secondary_color', 'image', 'series')
Views:
class SeriesView(viewsets.ModelViewSet):
serializer_class = SeriesSerializer
queryset = Series.objects.all()
class RosterView(viewsets.ModelViewSet):
serializer_class = RosterSerializer
queryset = Roster.objects.all()
Unsure where I am mistepping here.
So it turns out that all I needed to do was remove
series = SeriesSerializer(many=True, read_only=True)
and adjust my series field to
series = models.ForeignKey(Series, on_delete=models.CASCADE, blank=True, null=True)
No idea why this ended up working though so an explanation would still be cool.

Django call 'id' expected a number but got string

Django errors with django-import-export libraries.
I want to import data from excel to db via django admin. I use for it django-import-export, but i got Field 'id' expected a number but got 'HPI'.
Excel file contains
I found answer, that I have to add exclude = ('id',), but it didn't help. Also i did migrations, it didn't help too.
How to fix it and have ability to import 6 columns data from excel to db via django admin?
models.py
from django_mysql.models import JSONField, Model
from django.db import models
class Category(Model):
title = models.CharField(max_length=100)
class Meta:
ordering = ('-id',)
verbose_name = 'Category'
verbose_name_plural = 'Categories'
def __str__(self):
return self.title
class Tag(Model):
title = models.CharField(max_length=100)
class Meta:
ordering = ('-id',)
def __str__(self):
return self.title
class Type(Model):
title = models.CharField(max_length=100)
class Meta:
ordering = ('-id',)
verbose_name = 'Type'
verbose_name_plural = 'Types'
def __str__(self):
return self.title
class Macro(Model):
type = models.ForeignKey(
Type,
max_length=100,
null=True,
blank=True,
on_delete=models.SET_NULL)
tags = models.ManyToManyField(Tag, blank=True)
category = models.ForeignKey(
Category, null=True, blank=True, on_delete=models.SET_NULL)
abbreviation = models.CharField(max_length=100, unique=True)
title = models.CharField(max_length=100, verbose_name='Title')
content = models.TextField(max_length=1000, null=True, blank=True)
class Meta:
ordering = ('-id',)
def __str__(self):
return self.title
admin.py
from django.contrib import admin
from import_export import resources
from import_export.admin import ImportExportModelAdmin
from .models import Category, Tag, Type, Macro
class MacroResource(resources.ModelResource):
class Meta:
model = Macro
skip_unchanged = True
report_skipped = True
exclude = ('id', )
export_order = ('type', 'tags', 'category', 'abbreviation', 'title', 'content')
#admin.register(Macro)
class MacroAdmin(ImportExportModelAdmin):
resource_class = MacroResource
list_display = ('id', 'type', 'tags_list', 'category', 'abbreviation', 'title', 'content')
search_fields = ('title', 'category__title', 'type__title', 'abbreviation', 'content', )
def tags_list(self, obj):
tags = [t for t in obj.tags.all()]
return ' '.join(str(tags)) if tags else '-'
#admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('id', 'title')
#admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ('id', 'title')
def __str__(self):
return self.title
#admin.register(Type)
class TypeAdmin(admin.ModelAdmin):
list_display = ('id', 'title')
The problem was with ForeignKey and ManyToMany fields of a database model. So django-import-export library need to get widgets for this fields.
More about it here: https://django-import-export.readthedocs.io/en/latest/api_widgets.html#import_export.widgets.ForeignKeyWidget
Solution:
admin.py
class MacroResource(resources.ModelResource):
type = fields.Field(
column_name='type',
attribute='type',
widget=ForeignKeyWidget(Type, 'title'))
category = fields.Field(
column_name='category',
attribute='category',
widget=ForeignKeyWidget(Category, 'title'))
tags = fields.Field(
column_name='tags',
attribute='tags',
widget=ManyToManyWidget(Tag, field='title'))
class Meta:
model = Macro
skip_unchanged = True
report_skipped = True
exclude = ('id', )
import_id_fields = ('title',)
fields = ('type', 'tags', 'category', 'abbreviation', 'title', 'content')
instead of
class MacroResource(resources.ModelResource):
class Meta:
model = Macro
skip_unchanged = True
report_skipped = True
exclude = ('id', )
export_order = ('type', 'tags', 'category', 'abbreviation', 'title', 'content')
Django-import-export expects the first column to be id.
If these are new objects, simply leave the id column blank. Otherwise, put the database id of the object in that field.
If you're not able to modify the file, or don't want to, and you will always be adding new rows to the database (not modifying existing ones), you can create an id field dynamically in your resource class by overriding the method before_import and forcing get_instance to always return False.
class MacroResource(resources.ModelResource):
def before_import(self, dataset, using_transactions, dry_run, **kwargs):
dataset.insert_col(0, col=["",]*dataset.height, header="id")
def get_instance(self, instance_loader, row):
return False
class Meta:
model = Macro
skip_unchanged = True
report_skipped = True
export_order = ('type', 'tags', 'category', 'abbreviation', 'title', 'content')
The attributes where you are using a foreign key in the model,
you need to specify the id of the parent model and not the value in the xlsx/csv file.

ValueError when trying to create through Django REST Framework

I get a ValueError Cannot assign "[]": "Match.stats" must be a "Stats" instance. when I try and create a match through the browsable API but can create one through the shell just fine.
If I remove the HyperlinkedRelatedField from the MatchSerializer it can create just fine.
models.py
class Player(models.Model):
name = models.CharField(max_length=30)
account = models.IntegerField()
place = models.CharField(max_length=30)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='players')
def __str__(self):
return self.name
class Match(models.Model):
game = models.IntegerField()
length = models.IntegerField()
win = models.BooleanField()
player = models.ForeignKey(Player, on_delete=models.CASCADE, related_name='matches')
def __str__(self):
return str(self.game)
class Stats(models.Model):
goals = models.IntegerField()
assists = models.IntegerField()
time = models.IntegerField()
match = models.OneToOneField(Match, on_delete=models.CASCADE, related_name='stats')
def __str__(self):
return '{} {}'.format(str(self.goals), str(self.match))
class Team(models.Model):
possession = models.IntegerField()
goals = models.IntegerField()
assists = models.IntegerField()
extra = models.BooleanField(default=False)
match = models.OneToOneField(Match, on_delete=models.CASCADE, related_name='teams')
def __str__(self):
return '{} - {}'.format(str(self.possession), str(self.match))
serializer.py
class UserSerializer(serializers.ModelSerializer):
players = serializers.HyperlinkedRelatedField(many=True, view_name='players-detail', queryset=Player.objects.all())
class Meta:
model = User
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'players')
class PlayerSerializer(serializers.ModelSerializer):
user = serializers.ReadOnlyField(source='user.username')
matches = serializers.HyperlinkedRelatedField(many=True, view_name='matches-detail', queryset=Match.objects.all())
class Meta:
model = Player
fields = ('id', 'name', 'account', 'place', 'user', 'matches')
class MatchSerializer(serializers.ModelSerializer):
player = serializers.ReadOnlyField(source='player.name')
stats = serializers.HyperlinkedRelatedField(many=True, view_name='stats-detail', queryset=Stats.objects.all())
teams = serializers.HyperlinkedRelatedField(many=True, view_name='teams-detail', queryset=Team.objects.all())
class Meta:
model = Match
fields = ('id', 'game', 'length', 'win', 'player', 'stats', 'teams')
class StatsSerializer(serializers.ModelSerializer):
match = serializers.ReadOnlyField(source='match.game')
class Meta:
model = Stats
fields = ('id', 'goals', 'assists', 'time', 'match')
class TeamSerializer(serializers.ModelSerializer):
match = serializers.ReadOnlyField(source='match.game')
class Meta:
model = Team
fields = ('id', 'possession', 'goals', 'assists', 'extra', 'match')
I can go into python manage.py shell and create a match just fine.
>>>m = Match(game=12345, length=5674, win=True, player=player1) # a previously queried player
>>>m.save()
I'm just a little confused what is going
By default, HyperlineRelatedField allow both read and write operation, so I think you need make it read-only true:
class MatchSerializer(serializers.ModelSerializer):
player = serializers.ReadOnlyField(source='player.name')
stats = serializers.HyperlinkedRelatedField(view_name='stats-detail', read_only=True)
teams = serializers.HyperlinkedRelatedField(view_name='teams-detail', read_only=True)
class Meta:
model = Match
fields = ('id', 'game', 'length', 'win', 'player', 'stats', 'teams')
Also, you don't need to add many=True, because both teams and stats are OneToOne relations. So one entry for both tables will be created for each match.

Django REST Framework ModelSerializer read_only_fields not working

I have the following ListCreateAPIView
class TodoAPI(generics.ListCreateAPIView):
permission_classes = (IsAuthenticated, )
serializer_class = TodoSerializer
def get_queryset(self):
user = self.request.user
return Todo.objects.filter(user=user)
And in my serializers.py, I have
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = ('id', 'title', 'description',
'completed', 'created_at')
read_only_fields = ('id', )
But the problem is when I POST data into the form, I get the following error:
IntegrityError at /todo/
NOT NULL constraint failed: todo_todo.user_id
models.py
class Todo(models.Model):
user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
title = models.TextField(max_length=50)
description = models.TextField(max_length=200, blank=True, null=True)
completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
The problem is not with id field, but with user field. This field is not nullable in DB and since is required. You can just pass current user as defalt, for this use CurrentUserDefault:
class TodoSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Todo
fields = ('id', 'title', 'description',
'completed', 'created_at', 'user')

Categories