Django: Query multiple models based on parent model - python

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__()

Related

Calling a model's method inside a serializer and handling a received object

I'm supposed to write an API for the endpoints. It should be an application inside an existing project. I should work with its models and i'm not allowed to alter them in any way.
The project consists of multiple applications, and some applications have their own models.
There is an exempt from CategoryMetall/models.py in the CatalogNew application:
class CategoryMetall(MPTTModel):
position = models.ForeignKey(
Menu,
on_delete=models.CASCADE,
verbose_name="foo",
blank=True,
null=True,
)
subPosition = TreeForeignKey(
"self",
on_delete=models.CASCADE,
verbose_name="bar",
blank=True,
null=True,
)
def parent(self):
if self.subPosition:
return self.subPosition
else:
return self.position
As i understood, the parent() method is supposed to return an object of either a CategoryMetall model, or a Menu model. A Menu model is a model of another application from the project.
Here is an exempt from it as well:
Menu/models.py
class Menu(models.Model):
parent = models.ForeignKey(
"self",
on_delete=models.CASCADE,
verbose_name="parent category",
null=True,
blank=True,
)
So, i figured that in order to get a parent category i'm supposed to use the CategoryMetall.parent() method written by some other developer.
The issue is, i'm also supposed to somehow serialize it.
I have written a serializer in my serializers.py:
class CategoryMetallSerializer(serializers.HyperlinkedModelSerializer):
parentCategory = serializers.ReadOnlyField(source='parent')
class Meta:
model = CategoryMetall
fields = ['id', 'name', 'parentCategory']
And a view for it, views.py:
class CategoryMetallViewSet(viewsets.ModelViewSet):
queryset = CategoryMetall.objects.all()
serializer_class = CategoryMetallSerializer
pagination_class = CustomPagination
I have registered a url for this view in my urls.py as well:
router.register(r'catmet', views.CategoryMetallViewSet)
urlpatterns = [
path('myapi/', include(router.urls)),
]
The thing is, when i go to myapi/catmet link to see how it looks, i get an exception:
TypeError: Object of type Menu is not JSON serializable
As i understood, when i use
serializers.ReadOnlyField(source='parent')
it calls the parent() method of a CategoryMetall model, then returns by a foreign key an object of Menu model. It goes straight into a serializer and breaks because its somehow not serializable.
An object itself, as i got it from the debug screen, looks like this:
<Menu: Metallurgy raw materials >
I'm not sure if i'm using the right approach to call the method from the serializer, and even if i do, i have no idea what to do to serialize that.
I tried to search the Django Rest Framework documentation, google, reddit and StackOverflow to find out how to do it properly, or what exactly i do wrong, but failed. I'm still an intern, so i dont have an extensive knowledge of the framework and only started working with it like a week ago.
I investigated on how to serialize the foreign key itself and found out that its done by writing another serializer specifically for the model a foreign key refers to, then using it inside the main one. But i don't know how to do that in this case, or if it even is a solution.
Can you please suggest something?
As i understood, when i use `serializers.ReadOnlyField(source='parent')` it calls the parent() method of a CategoryMetall model, then returns by a foreign key an object of Menu model
That's correct.
The problem with the parent method is that it returns one of two model types: Menu or CategoryMetall (self).
I personally see only the option to return both objects in the API call and then check later in the app or whatever this is used if the subPosition is available or not.
With this approach you can define a new serializer for the Menu. Django doesn't know how to return <Menu: Metallurgy raw materials > as JSON. You have to tell it which fields it should serialize. Exactly like in the CategoryMetallSerializer. For example:
class MenuSerializer(serializers.ModelSerializer):
class Meta:
model = Menu
fields = ['field_1', 'field_2'] # all fields you want to fetch from the menu
Now you can use this serializer inside the CategoryMetallSerializer:
class CategoryMetallSerializer(serializers.HyperlinkedModelSerializer):
position = MenuSerializer(read_only=True)
subPosition = CategoryMetallSerializer(read_only=True)
class Meta:
model = CategoryMetall
fields = ['id', 'name', 'position', 'subPosition']
I've typed this out of my head. There might be some syntax issues in the code as it is not tested but I hope I could point you in the right direction. BTW +1 for the details in your question.
EDIT 1 (comment 1: only serialize one field)
If you want to change the output of the serializer, you can override the to_representation function of the serializer like that:
class CategoryMetallSerializer(serializers.HyperlinkedModelSerializer):
position = MenuSerializer(read_only=True)
subPosition = CategoryMetallSerializer(read_only=True)
class Meta:
model = CategoryMetall
fields = ['id', 'name', 'position', 'subPosition']
def to_representation(self, instance):
data = super().to_representation(instance)
print(data) # for debug reasons
# modify data as you wish - I'm actually not sure if this is a dict and if the following works
if data.get('subPosition'):
del data['position']
else:
del data['subPosition']
return data
Another approach would be something in this way from the official docs:
https://www.django-rest-framework.org/api-guide/relations/
def to_representation(self, value):
"""
Serialize bookmark instances using a bookmark serializer,
and note instances using a note serializer.
"""
if isinstance(value, Bookmark):
serializer = BookmarkSerializer(value)
elif isinstance(value, Note):
serializer = NoteSerializer(value)
else:
raise Exception('Unexpected type of tagged object')
return serializer.data

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 many-to-many relation create the link

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)

How to do proper reverse querying when using Django generic relations?

I have implemented django generic relations to create relations between models. My models are,
class CessPoint(BaseModel):
....
title = models.CharField(max_length=100)
class BilledVehicle(BaseModel):
....
source = models.ForeignKey(CessPoint, on_delete=models.CASCADE)
class Bill(BaseModel):
.....
content_type = models.ForeignKey(
ContentType, on_delete=models.CASCADE, null=True)
object_id = models.UUIDField(null=True)
billed_item = GenericForeignKey()
class BillPayment(BaseModel):
.....
bill = models.ForeignKey(Bill, on_delete=models.CASCADE)
I would like to get the payments for BilledVehicle. This is how I would like to go about it.
BillPayment.objects.filter(bill__content_type=ContentType.objects.get_for_model(BilledVehicle)).values('bill__billed_item__source__title').annotate(total_paid=Sum('amount_paid')).order_by('-total_paid')
I am getting the error:
Field 'billed_item' does not generate an automatic reverse relation and therefore cannot be used for reverse querying. If it is a GenericForeignKey, consider adding a GenericRelation.
According to this answer, How to traverse a GenericForeignKey in Django?, defining a GenericRelation might solve my problem. But then again, I did not define a GenericRelation because adding one will cascade all relations as per the default behavior
Unlike ForeignKey, GenericForeignKey does not accept an on_delete argument to customize this behavior; if desired, you can avoid the cascade-deletion simply by not using GenericRelation, and alternate behavior can be provided via the pre_delete signal.
If you do not want to create a GenericRelation object in your BilledVehicle module, django will have no way of linking it directly to your Bill using values.
You can work around this by using a dictionary to store unique CessPoint titles and their total (do the annotation yourself) from the BillPayment queryset amount paid as follows.
qs = BillPayment.objects.filter(
bill__content_type=ContentType.objects.get_for_model(BilledVehicle))
ds = {}
for billPayment in qs:
title = billPayment.billed_item.source.title
ds[title] = ds.get(title, 0) + billPayment.amount_paid

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])

Categories

Resources