Django ModelForm with foreign key - python

I'm trying to create a ModelForm that updates a table with foreign keys. What I have seems to work, but I was hoping someone could tell me if there's a better way to do this or if there's a problem with the way I'm doing it below.
Is it correct to use the queryset on the Author and Genres table? It feels like I should be using a queryset on the Book model, and relate the foreign key to those tables.
models.py:
class Author(models.Model):
name = models.CharField(max_length=200)
class Genre(models.Model):
name = models.CharField(max_length=200)
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey('Author')
genre = models.ForeignKey('Genre')
forms.py:
class BookForm(ModelForm):
title = forms.CharField(max_length=200)
author = forms.ModelChoiceField(queryset=Author.objects.all())
genre = forms.ModelChoiceField(queryset=Genre.objects.all())
class Meta:
model = Book
fields = ['title', 'author', 'genre']
views.py:
def add_book(request):
if request.method == 'POST':
form = BookForm(request.POST)
if form.is_valid():
form.save(commit=True)
return HttpResponseRedirect('/add/')
else:
form = BookForm()

The only thing that's wrong with this code is you are overriding the default behaviour of your model form.
Change it to look like:
class BookForm(ModelForm):
class Meta:
model = Book
fields = ['title', 'author', 'genre']
And let django handle with the definition of those.
if you need to add labels or widgets, you can define them in the Meta class:
class BookForm(ModelForm):
class Meta:
model = Book
fields = ['title', 'author', 'genre']
labels = {'title': 'Book title', }
For example.

I'm not really sure what you're asking here, or why you would want a queryset on Book.
You haven't actually changed any of the defaults on any of the fields, so there is no need to redefine them: your form could be simply
class BookForm(ModelForm):
class Meta:
model = Book
and it would work exactly the same.

Related

Django REST Framework : ManyToMany relationship

I'm currently working on a Django project, I'm new at this and it's difficult to find clear documentation about this.
My issue is very simple, I want at the creation of my model, automatically associate another model through a ManyToMany relationship.
Here goes the code :
Model :
class Favorite(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=60, blank=False)
estates = models.ManyToManyField(Estate, blank=True)
Serializer :
class FavoriteSerializer(serializers.ModelSerializer):
estates = EstateSerializer(read_only=True, many=True)
class Meta:
model = Favorite
fields = ['uuid', 'title', 'estates']
def create(self, validated_data):
instance = super(FavoriteSerializer, self).create(validated_data)
instance.save()
return instance
ViewSet :
class MyFavoriteEstatesListViewSet(viewsets.ModelViewSet):
serializer_class = FavoriteSerializer
def get_queryset(self):
return Favorite.objects.filter(users__id=self.request.user.id)
I'm currently sending something like this through a POST :
{"title": "some title", "estate_uuid": "XXX"}
I just wanted to instantiate my Estate model with the UUID I just sent and adding it with
favorite.estates.add(estate)
How can I achieve this ?
Thanks for your help !
I think you can try something like this:
class EstateField(serializers.RelatedField):
def to_representation(self, obj):
return EstateSerializer(obj).data
def to_internal_value(self, data):
return Estate.objects.get(pk=data)
class FavoriteSerializer(serializers.ModelSerializer):
estates = EstateField(many=True, queryset=Estate.objects.all())
class Meta:
model = Favorite
fields = ['uuid', 'title', 'estates']
I am using Custom Fields to represent the data as dictionary, but take input as uuid.

How do you add existing objects to a ManyToMany field using Serializer?

I am trying to create an API with Artists and Songs, with a ManyToMany relationship between the two. Using the API to create a Song with an Artist that is not in the database works fine. The problem arises when I attempt to use the POST method to create a new Song with an Artist that already exists in the database. I tried overwriting the SongSerializer create() method using get_or_create() based on another post here, but I kept getting Bad Request errors when the Artist already exists in the database. The relevant code snippets:
models.py
class Artist(models.Model):
artist_name = models.CharField(max_length=200, unique=True)
class Meta:
ordering = ['artist_name']
def __str__(self):
return self.artist_name
class Song(models.Model):
song_title = models.CharField(max_length=200)
artists = models.ManyToManyField(Artist, related_name='songs')
class Meta:
ordering = ['song_title']
def __str__(self):
return self.song_title
serializers.py
class ArtistNameSerializer(serializers.ModelSerializer):
class Meta:
model = Artist
fields = ('artist_name',)
def to_representation(self, value):
return value.artist_name
class SongTitleSerializer(serializers.ModelSerializer):
songs = serializers.PrimaryKeyRelatedField(read_only=True, many=True)
def to_representation(self, value):
return value.song_title
class Meta:
model = Song
fields = ('songs',)
class ArtistSerializer(serializers.HyperlinkedModelSerializer):
songs = SongTitleSerializer(read_only=True, many=True)
class Meta:
model = Artist
fields = ('id', 'artist_name', 'songs')
class SongSerializer(serializers.HyperlinkedModelSerializer):
artists = ArtistNameSerializer(many=True)
class Meta:
model = Song
fields = ('id', 'song_title', 'artists',)
def create(self, validated_data):
artist_data = validated_data.pop('artists')
song = Song.objects.create(**validated_data)
song.save()
for artist_item in artist_data:
a, created = Artist.objects.get_or_create(artist_name=artist_item['artist_name'])
song.artists.add(a)
return song
I've done some tests and it looks like the program doesn't even go into the create() method I'm using, going straight to showing me the Bad Request error. What am I missing? Thanks in advance!
On you Artist model you have a constrain on the artist_model field (unique=True)
if you print the serializer in question with:
print(SongSerializer())
you get something like this:
SongSerializer():
id = IntegerField(label='ID', read_only=True)
song_title = CharField(max_length=200)
artists = ArtistNameSerializer(many=True):
artist_name = CharField(max_length=200, validators=[<UniqueValidator(queryset=Artist.objects.all())>])
under the artist_name field is a Validator "UniqueValidator"
so in case of a write operation you can disable the validator in the serializer with:
class ArtistNameSerializer(serializers.ModelSerializer):
class Meta:
model = models.Artist
fields = ('artist_name',)
extra_kwargs = {
'artist_name': {
'validators': [],
}
}
hope this help..

How to get model data to appear as a field in another model's response

These are simplified versions of my models (the user model is just an id and name)
class Convo(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='convo_owner')
users = models.ManyToManyField(User, through='Convo_user')
class Convo_user (models.Model):
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
convo = models.ForeignKey(Convo, on_delete=models.CASCADE)
class Comments(models.Model):
name = models.CharField(max_length=255)
content = models.TextField(max_length=1024)
convo = models.ForeignKey(Convo, on_delete=models.CASCADE)
This is my view
class ConvoViewSet(viewsets.ModelViewSet):
serializer_class = serializers.ConvoSerializer
def get_queryset(self):
return None
def list(self, request):
curr_user = request.user.id
# Collecting the list of conversations
conversations = models.Conversation.object.filter(ConvoUser__user_id=request.user.id)
#Getting list of conversation id's
conv_ids = list(conversations.values_list('id', flat=True).order_by('id'))
#Getting list of relevant comments
comments = models.Comments.objects.filter(conversation_id__in=conv_ids)
return Response(self.get_serializer(conversations, many=True).data)
And my current serializer
class ConvoSerializer(serializers.ModelSerializer):
"""A serializer for messaging objects"""
# access = AccessSerializer(many=True)
# model = models.Comments
# fields = ('id', 'name', 'content', 'convo_id')
class Meta:
model = models.Convo
fields = ('id', 'owner_id')
The current response I get is of the form
[
{
"id": 1,
"owner_id": 32
}, ...
]
But I would like to add a comments field that shows all the properties of comments into the response, so basically everything in the second queryset (called comments) and I'm not sure how to go about this at all. (I retrieve the comments in the way I do because I'm trying to minimize the calls to the database). Would I need to create a new view for comments, make its own serializer and then somehow combine them into the serializer for the convo?
The way you've set up your models, you can access the comments of each Convo through Django's ORM by using convo_object.comments_set.all(), so you could set up your ConvoSerializer to access that instance's comments, like this:
class ConvoSerializer(serializers.ModelSerializer):
"""A serializer for messaging objects"""
comments_set = CommentSerializer(many=True)
class Meta:
model = models.Convo
fields = ('id', 'owner_id', 'comments_set')
and then you define your CommentSerializer like:
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = models.Comments
fields = ('id', 'name', 'content')
No data appears because my serializers are using the default database, not sure why but a step forward
EDIT:
Django: Database used for prefetch_related is not the same that the parent query Provided me the correct answer, I was able to choose the database with this method because for some reason inner queries use the default DB

Django REST framework - Getting data instead of links

I have taken a look around, and I didn't find an answer to this question.
Serializers.py
class PostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Post
fields = ['title', 'body', 'comments', 'user', 'date']
class CommentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Comment
fields = ['body', 'user', 'date']
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['id', 'user']
Models.py
class Post(models.Model):
# Children
comments = models.ManyToManyField('Comment', blank=True)
# Content
title = models.TextField(default="-->Title here<--")
body = models.TextField(default="-->Body here<--")
# About
user = models.ForeignKey(User)
date = models.DateTimeField(auto_now=True)
object_id = models.PositiveIntegerField(default=0)
content_type = models.ForeignKey(ContentType, default=0)
content_object = fields.GenericForeignKey()
def __str__(self):
return str(self.title)
class Comment(models.Model):
comments = models.ManyToManyField('Comment', blank=True)
# Content
body = models.TextField(default="-->Body here<--")
# About
user = models.ForeignKey(User)
date = models.DateTimeField(auto_now=True)
object_id = models.PositiveIntegerField(default=0)
content_type = models.ForeignKey(ContentType, default=0)
content_object = fields.GenericForeignKey()
def __str__(self):
return str(self.body)
Views.py
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def index(request):
return render(request, 'index.html', [])
The index template is a "homepage" that loads the latest posts. Under each post, I'm showing the comments. In the JSON those are links, and I found out how to load them trougth the links (so it's working).
Someone told me that instead of doing it this way, I should make it "load" the comments in the backend, and send them together with the posts (the data, not links). He told me to take a look into: http://www.django-rest-framework.org/api-guide/filtering/#overriding-the-initial-queryset
I can't really figure it out.
How do I get the data, insted of links for the ManyToManyField?
To unfold all the related data one level deep you can use depth param:
class PostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Post
fields = ['title', 'body', 'comments', 'user', 'date']
depth = 1
This will replace post.user and post.comments ids with actual records. depth=2 will also unfold post.comments.user. If you want to selectively pull post.comments only one level deep and not post.user:
class PostSerializer(serializers.HyperlinkedModelSerializer):
comments = CommentSerializer(many=True) #has to be declared higher above
class Meta:
model = Post
fields = ['title', 'body', 'comments', 'user', 'date']
If on top you want to have post.comments.user unfolded, need to either put depth=1 into existing comment serializer or create a new serializer with unfolded users just for this view, similar to examples above.
Also make sure you are using prefetch_related on your queryset or the performance will take a serious hit, like:
Post.objects.all().prefetch_related('comments', 'comments__user')

Django Rest Framework: dynamic database on POST - RelatedField or PrimaryKeyRelatedField

I'm developing RESTFul services with DRF and I have multiple databases depending on the country (see my last question here)
I'm having a problem now with relationships, I have two models: Category and SubCategory:
class SubCategory(models.Model):
objects = CountryQuerySet.as_manager()
id = models.AutoField(primary_key=True,db_column='sub_category_id')
name = models.TextField()
female_items_in_category = models.BooleanField()
male_items_in_category = models.BooleanField()
kids_items_in_category = models.BooleanField()
category = models.ForeignKey('Category')
class Meta:
managed = True
db_table = Constants().SUBCATEGORY
And the serializer is:
class SubCategorySerializer(serializers.ModelSerializer):
category = PrimaryKeyRelatedField(queryset=Category.objects.using('es').all())
class Meta:
model = SubCategory
fields = ('id', 'name','female_items_in_category','male_items_in_category','kids_items_in_category','category')
If I don't set the queryset with the proper country it fails, because it doesn't know where to get the category.
Here the problem
I already set the country in the serializer context (in the ModelViewSet):
def get_serializer_context(self):
return {Constants().COUNTRY: self.kwargs.get(Constants().COUNTRY)}
But I can not find the proper way to get the self.context.get(Constants().COUNTRY) in the serializer.
Do you any have an idea to solve this? Thanks!
Well, I found a solution to my problem: I overwrite the method get_fields in the serializer:
def get_fields(self, *args, **kwargs):
fields = super(SubCategorySerializer, self).get_fields()
country = self.context.get(Constants().COUNTRY)
qs = Category.objects.using(country).all()
fields['category'].queryset = qs
return fields
And that works!

Categories

Resources