Django Rest Framework many-to-many relation create the link - python

Since there are a few questions about m2m and DRF I'll try narrow down what specifically I'm interested in. Let's call the two models 'article' and 'publication'. Assume that:
The 'publication' object already exists.
The 'article' object may or may not exist. specifically:
a) If a previous publication contained the article, then it will already be
there.
b) If not, then the article will need to be created.
I want to send a post http request with the article data in the body
and the publication id available from the url which will:
a) if the article already exists, link it to the publication
b) if the article does not exist, create it, and then link it to the publication
Going for the 'default' strategy below did not work out. I can think of two ways to approach this problem:
Overriding the create method on the article serializer. However I'm scepticle of doing that since this seems like a problem that should be common and have a non-custom solution.
Creating an endpoint to directly work with the 'through' model. I could then split up the process into two steps (and 2 requests) where I first get_or_create the article, and then post to the through model endpoint to create the link.
Are there any other approaches or built-in DRF solutions to this problem?
Here's where I'm at currently:
models.py
class Publication(models.Model):
name = models.CharField(max_length=255, unique=True)
collection = models.CharField(max_length=255)
class Article(models.Model):
major = models.IntegerField()
minor = models.IntegerField()
publication = models.ManyToManyField(Publication)
class Meta:
constraints = [models.UniqueConstraint(fields=['major', 'minor'], name='unique_article')]
views.py
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
queryset = Article.objects.all()
serializers.py
class ArticleSerializer(serializers.ModelSerializer):
publication = serializers.SlugRelatedField(slug_field='name', queryset=Publication.objects.all()), many=True)
class Meta:
model = Article
fields = '__all__'
When posting to this endpoint I'll get a 'duplicate entry' integrity error if the article does already exist, instead of the article then just being linked.

This is the way I have handled this issue in the past. If your using the Primary keys these calls are not very expensive.
pub = Publications.objects.get(id=1)
article, created = Articles.objects.get_or_create(
id=1,
defaults= {other_params:'value', param : 'value'},
)
pub.articles.add(article)

Related

DJango rest framework - API list using filter field from related models

Hi I'm new to Django and the Django rest framework so my terminology may be off.
I'm trying to build an API that gives back a list of items from a model but filtered based on fields in another related model.
I'll provide my current view and serializer classes and models
class service(models.Model):
name = models.CharField(max_length=50)
vendor = models.CharField(max_length=50)
version = models.CharField(max_length=10)
registration_status = models.BooleanField(default=False)
class service_network(models.Model):
service = models.OneToOneField(
service,
related_name='network',
on_delete=models.CASCADE,
primary_key=True,
)
forwarded_port = models.CharField(max_length=50)
class ServiceNetworkSerializer(serializers.ModelSerializer):
class Meta:
model = service_network
fields = '__all__'
class ServiceSerializer(serializers.ModelSerializer):
network = ServiceNetworkSerializer()
class Meta:
model = service
fields = [
'id',
'name',
'vendor',
'version',
'registration_status',
'network',
]
class ServiceAPI(ModelViewSet):
queryset = service.objects.all()
serializer_class = ServiceSerializer
filterset_fields = '__all__'
Currently I can get back lists using a URL query string
{{baseUrl}}/engine/service?registration_status=true
What I want to do is something like this
{{baseUrl}}/engine/service/network?forwarded_port=8080
Which I would expect to give back a list of services where the related network field "forwarded_port" is equal to 8080.
Is there another way to query this API? Maybe using a POST with a body containing the query? If there something in the DOCS that I can read, I've tried to look through filtering and querysets but I wasn't able to find anything that would do this out of the box
I'm also new to stackoverflow and I've tried to keep my question short with as much relevant information so if there anything missing I'd be happy to edit my question
I was able to solve this using the following queryset override
def get_queryset(self):
if len(self.request.GET) > 0:
query_set = {}
for query in self.request.GET:
query_set[query] = self.request.GET.get(query)
return service.objects.filter(**query_set)
else:
return service.objects.all()
What this does is lets you filter fields without explicitly specifying what they are, in cases when you have many fields that need filtering. I also have to say as I'm not experienced with Django, I'm not sure what kind of errors this may bring up but its a hack that's worked for me. If I find this is really bad I'll come back and remove this.
Try this:
{{baseUrl}}/engine/service?network__forwarded_port=8080
Probably it works.
Doc: https://docs.djangoproject.com/en/dev/topics/db/queries/#lookups-that-span-relationships-1
EDIT:
If the above answer doesn't work, you can change the ServiceApi class and filter by yourself:
class ServiceAPI(ModelViewSet):
def get_queryset(self):
if self.request.GET.get(network__forwarded_port)
return service.objects.filter(network__forwarded_port = self.request.GET.get(network__forwarded_port))
else:
return service.objects.all()
serializer_class = ServiceSerializer
filterset_fields = '__all__'

Django Rest Framework creating child objects in parent serializer using child serializer

Supposing some standard Django relational setup like this:
models.py
class Book(models.Model):
title = models.CharField(max_length=30)
class Page(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
text = models.CharField(max_length=100)
I'd like to create a book and all its pages with one request. If we start with serializers like this:
serializers.py
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = '__all__'
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('title', 'pages')
pages = PageSerializer(many=True)
Then the problem is that the PageSerializer now requires a book foreign key. But I don't know the key of the book until I've created the book, which is only after I've sent the POST request. So I cannot include the book pk in the POST data that the client sends.
An obvious solution is to override the create function on the Book serializer. But then I am still faced with the problem that the validators will say that the book field is required and the POST data will fail to validate.
I could make book a not-required field on the PageSerialzer. But this seems very bad. The book field IS required. And the BookSerializer create method will be able to supply it. It's just the client that doesn't know it.
So my suspicion is that the best way to do this is to leave book as required on the PageSerializer, but somehow make it so that the validators on the BookSerializer don't check for whether that is in the POST data when I post to BookSerializer.
Is this the correct way to achieve what I want? And if so, how do I do it? Thank you.
Why not try handling it in the create viewset. You can validate the data for the Book object first, before creating it. Then validate the data for the Page object using the created Book object and the other data sent from the request to the page.
I'd link your ViewSet to a BookCreateSerializer, and from this specific serializer I'd then add a function to not only verify the received data but make sure you link the parent's id to the child's one during creation.
IMPORTANT NOTE
This works if a parent only has one child, not sure about when passing multiple children.
Here is what is could look like.
BookCreateSerializer:
class BookCreateSerializer(serializers.ModelSerializer):
"""
Serializer to create a new Book model in DB
"""
pages = PageCreateSerializer()
class Meta:
model = Book
fields = [
'title',
'pages'
]
def create(self, validated_data):
page_data = validated_data.pop('page')
book = Book.objects.create(**validated_data)
Page.objects.create(book=book, **page_data)
return book
PageCreateSerializer
class PageCreateSerializer(serializers.ModelSerializer):
"""
Serializer to create a new Page model in DB
"""
class Meta:
model = Page
fields = [
'book',
'text'
]
To make sure that your Book instance understands what a page field is in the serializer, you have to define a related_name in its child's Model (Page). The name you choose is up to you. It could look like:
class Page(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='page')
text = models.CharField(max_length=100)

Django: Query multiple models based on parent model

I'm creating a blog in Django where I have a base model PostType which I then extend in to several subclasses for different types of content on the website. For example CodeSnippet and BlogPost.
The idea is that these content types are mostly the same, they all have an author, a title, a slug, etc, but they also have a few unique fields. For example a blog post has a field for the text content, while a code snippet has a related field for programming language.
Something like this:
class PostType(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
title = models.CharField(
max_length=255,
unique=True,
)
class Meta:
abstract = True
class BlogPost(PostType):
content = models.TextField(
default='',
)
class GitHubRepo(PostType):
url = models.URLField(
unique=True
)
class CodeSnippet(PostType):
language = models.ForeignKey(
to=Language,
on_delete=models.CASCADE,
)
Now what I want to know is if there's any good/prefered way to query all objects in the database that are based on the parent class PostType?
For the site's search I am currently querying each of the different content types, and then merging the result. This is the code for the search view:
class Search(View):
def get(self, request):
context = {}
try:
query = request.GET.get('s')
blog_list = models.BlogPost.objects.filter(title__icontains=query)
projects_list = models.Project.objects.filter(title__icontains=query)
code_list = models.CodeSnippet.objects.filter(title__icontains=query)
from itertools import chain
context['result_list'] = list(chain(blog_list, projects_list, code_list))
except KeyError:
query = ''
context['title'] = 'Result for "{}"'.format(query)
return render(request, 'blog/search.html', context)
This all works fine, but I would like to know if there's any way to query all children of PostType at the same time?
Is Django somehow aware of what child models exist? And can I use that somehow?
Like a PostType.child_objects.get() or something similar.
Even a way to programmatically get all the children so that I could loop through them and get all the objects would be fine too.
For the time being I just have a few models, but the number of child models were to increase, it would be great if I could be assured that all the models would be included in the site search automatically based on their relationship to their parent model.
PostType is an abstract Model (So, it does not create physical table. It's just to use inheritance feature in Django). As far as i understand you want to generate list of QuerySet's merge it in a single list and iterate over list/QuerySet later.
get_qs_list = [model.objects.filter(title__icontains=query) for model in PostType.__subclasses__()] # This contains QuerySet instances now.
for qs in get_qs_list:
# qs iterator returns QuerySet instance
for instance in qs:
# instance iterator is single Model instance from QuerySet collection
print(instance.title)
Hope, it helps you.
If PostType is not an abstract model then you should be able to query it directly to get all those subclass results
PostType.objects.filter(title__icontains=query)
Otherwise, you cannot really do this with a single query.
Even a way to programmatically get all the children so that I could
loop through them and get all the objects would be fine too.
This is possible --- to get the subclasses programmatically, you would do
PostType.__subclasses__()

queryset value for SlugRelatedField when unique_together applies in django-rest

I'm building a simple API for an ESP8266 to connect to in an IoT application, passing a JSON string. In this application there are multiple Monitors (internet connected devices) per Site (location/address), and multiple LogEntries per Site/Monitor.
The API was originally setup with an endpoint like:
/api/logentries/
Posting a JSON string like:
{"site":"abcd","monitor":"xyz","data_point":"value"}
In the object model, Monitor is a child of Site, but for convenience of entry creation and reporting, the JSON format of the LogEntry posted by each device flattens this structure out, meaning that the LogEntry model also has a FK relationship for both Site and Monitor. In the code below, "textID" is the ID used within the context of the API for the Site/Monitor (e.g. PK values remain "hidden" for API callers).
In models.py:
class Site(models.Model):
name = models.CharField(max_length=32)
textID = models.CharField(max_length=32, blank=True, db_index=True, unique=True)
class Monitor(models.Model):
textID = models.CharField(max_length=32)
site = models.ForeignKey(Site, on_delete=models.CASCADE)
class Meta:
unique_together = ('site', 'textID')
class LogEntry(models.Model):
site = models.ForeignKey(Site, on_delete=models.CASCADE)
monitor = models.ForeignKey(Monitor, on_delete=models.CASCADE)
data_point = models.CharField(max_length=8, default='')
To get this to work on a single site, I created a custom serializer:
class LogEntrySerializer(serializers.HyperlinkedModelSerializer):
site = serializers.SlugRelatedField(slug_field='textID', queryset=Site.objects.all())
monitor = serializers.SlugRelatedField(slug_field='textID', queryset=Monitor.objects.filter())
class Meta:
model = LogEntry
fields = ('pk', 'site', 'monitor', 'data_point', )
This works for reading valid data, and saving when all monitor IDs are unique across sites.
However, if two sites have a Monitor with the same textID—e.g. "Site1/001" and "Site2/001" this breaks, as the Monitor.objects.all() results in multiple records being retrieved (which makes sense and is expected behaviour).
What I'm wanting to do is to have the second queryset (for monitor) limited to the specified site, to avoid this error.
This post almost answers my question, however it benefits from the second field value (user) being available in the request object, something that is not available in this case.
Is there a way I can retrieve the Site.pk or Site.textID for the queryset value to resolve correctly--e.g. queryset=Monitor.objects.filter(site__textID=xxx)--what would 'xxx' be? Or do I need to completely override the serializer (and not rely on SlugRelatedField)? Or some other approach that might work?
(As an aside: I recognise that this could be achieved by modifying the URL pattern to something like /api///logentries, which would then have this information available as part of the request/context and from a normalisation perspective would be better also. However this would require reflashing of a number of already deployed devices to reflect the changed API details, so I'd like to avoid such a change if possible, even though upon reflection this is probably a cleaner solution/approach long-term.)
Thanks in advance.
You'll need to write your own SlugRelatedField subclass. The unicity constraint that applies to a SlugRelatedField doesn't apply to your case.
This can be done by creating a subfield and overriding the get_value to retrieve the site/monitor tuple and to_internal_value to select the appropriate monitor.
Thanks to the pointers from Linovia, the following field class resolves the issue:
class MonitorRelatedField(serializers.Field):
def to_representation(self, obj):
return obj.textID
def get_value(self, data):
site_textID = data['site']
monitor_textID = data['monitor']
return ( site_textID, monitor_textID, )
def to_internal_value(self, data):
return Monitor.objects.get(site__textID=data[0], textID=data[1])

How to POST to a model using django restframework

I'm new to Django and trying to play with restframework. I have created a simple model and I'd like to POST to this model via REST and sending JSON data.
This is what I've done so far:
models.py
class Contact(models.Model):
name = models.CharField(max_length=120)
email = models.EmailField()
phone = models.CharField(max_length=15)
city = models.CharField(max_length=120)
comment = models.CharField(max_length=500)
timestamp = models.DateTimeField(auto_now_add=True)
serializers.py
class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = ('name', 'email', 'phone', 'city', 'comment', 'timestamp')
urls.py
url(r'^api/contact/(?P<pk>[0-9]+)/$', ContactDetail.as_view()),
views.py
class ContactDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
format = None
but when I try to post to http://127.0.0.1:8001/api/contact I get this error
13. ^index.html#/verifyEmail/(?P<key>\w+)/$ [name='account_confirm_email']
14. ^api/contact/(?P<pk>[0-9]+)/$
The current URL, api/contact, didn't match any of these.
Question
How can I POST data to my model and save it?
You got a couple of problems here:
You are using generics.RetrieveUpdateDestroyAPIView which will provide the PUT, GET and DELETE methods. If you want to be able to POST (this means create) to that endpoint, you should be using another one. Replace it for viewsets.ModelViewSet, it will provide all CRUD methods. Don't forget to import the module (+more info).
You are trying to build the urls yourself which is not correct, drf provides routers to build them automatically. Just follow the docs, they are really well explained.
Once you fix those issues, you will be able to POST to /api/contact/ to create a new one.
Your main issue is that the regular expression here:
url(r'^api/contact/(?P<pk>[0-9]+)/$', ContactDetail.as_view())
does not match the URL:
http://127.0.0.1:8001/api/contact
A match should look more like the following:
http://127.0.0.1:8001/api/contact/123412213/
Including the trailing slash /

Categories

Resources