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])
Related
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
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)
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__()
I am a new user of Django, and I am trying to figure out how to created a model which can support many kind (type) of elements.
This is the plot : I want to create a Blog module on my application.
To do this, I created a model Page, which describe a Blog Page. And a model PageElement, which describe a Post on the blog. Each Page can contain many PageElement.
A PageElement can have many types, because I want my users could post like just a short text, or just a video, or just a picture. I also would like (for example) the user could just post a reference to another model (like a reference to an user). Depending of the kind of content the user posted, the HTML page will display each PageElement in a different way.
But I don't know what is the right way to declare the PageElement class in order to support all these cases :(
Here is my Page model :
class Page(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
# Basical informations
title = models.CharField(max_length=150)
description = models.TextField(blank=True)
# Foreign links
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name='pages_as_user'
)
created_at = models.DateTimeField(default=timezone.now)
# Other fields ....
class Meta:
indexes = [
models.Index(fields=['uuid']),
models.Index(fields=['user', 'artist'])
]
For now, I have two solutions, the first one use inheritance : When you create a new post on the blog, you create an Element which inherit from PageElement model. Here are my different Models for each cases :
class PageElement(models.Model):
page = models.ForeignKey(
Page,
on_delete=models.CASCADE,
related_name='%(class)s_elements'
)
updated_at = models.DateTimeField(default=timezone.now)
created_at = models.DateTimeField(default=timezone.now)
class PageImageElement(PageElement):
image = models.ImageField(null=True)
image_url = models.URLField(null=True)
class PageVideoElement(PageElement):
video = models.FileField(null=True)
video_url = models.URLField(null=True)
class PageTextElement(PageElement):
text = models.TextField(null=True)
class PageUserElement(PageElement):
user = models.ForeignKey(
'auth.User',
on_delete=models.CASCADE,
related_name='elements'
)
This solution would be the one I have choosen if I had to work with "pure" Python. Because I could stored each PageElement in a dictionnary and filter them by class. And this solution could be easily extended in the futur with new type of content.
But with Django models. It seems that is not the best solution. Because it will be really difficult to get all PageElement children from the database (I can't just write "page.elements" to get all elements of all types, I need to get all %(class)s_elements elements manually and concatenate them :/). I have thinked about a solution like below (I don't have tried it yet), but it seems overkilled for this problem (and for the database which will have to deal with a large number of request):
class Page(models.Model):
# ...
def get_elements(self):
# Retrieve all PageElements children linked to the current Page
R = []
fields = self._meta.get_fields(include_hidden=True)
for f in fields:
try:
if '_elements' in f.name:
R += getattr(self, f.name)
except TypeError as e:
continue
return R
My second "solution" use an unique class which contains all fields I need. Depending of the kind of PageElement I want to create, I would put type field to the correct value, put the values in the corresponding fields, and put to NULL all other unused fields :
class PageElement(models.Model):
page = models.OneToOneField(
Page,
on_delete=models.CASCADE,
related_name='elements'
)
updated_at = models.DateTimeField(default=timezone.now)
created_at = models.DateTimeField(default=timezone.now)
TYPES_CHOICE = (
('img', 'Image'),
('vid', 'Video'),
('txt', 'Text'),
('usr', 'User'),
)
type = models.CharField(max_length=60, choices=TYPES_CHOICE)
# For type Image
image = models.ImageField(null=True)
image_url = models.URLField(null=True)
# For type Video
video = models.FileField(null=True)
video_url = models.URLField(null=True)
# For type Text
text = models.TextField(null=True)
# For type User
user = models.ForeignKey(
'auth.User',
on_delete=models.CASCADE,
related_name='elements',
null=True
)
With this solution, I can retrieve all elements in a single request with "page.elements". But it is less extendable than the previous one (I need to modify my entire table structure to add a new field or a new kind of Element).
To be honnest, I have absolutly no idea of which solution is the best. And I am sure other (better) solutions exist, but my poor Oriented-Object skills don't give me the ability to think about them ( :( )...
I want a solution which can be easily modified in the future (if for example, I want to add a new Type "calendar" on the Blog, which reference a DateTime). And which would be easy to use in my application if I want to retrieve all Elements related to a Page...
Thanks for your attention :)
I'm not sure it fits your problem but using GenericForeignKeys/ContentType framework may be appropriate in this case. It's quite powerful when one grasps the concept.
Example construct:
class Page(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
page_element = GenericForeignKey('content_type', 'object_id')
...
You can now connect any model object by the GenericFK to the Page model. So adding a new type (as a new model), at a later stage, is not intrusive.
Update:
As a comment pointed out this construct doesn't support many PageElements in a good way for a Page.
To elaborate, one way to solve that problem, still taking advantage of the GenericFK...
class PageElement(models.Model):
class Meta:
unique_together=('page', 'content_type', 'object_id') # Solve the unique per page
page = models.ForeignKey(Page, related_name='page_elements')
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
A Page can have many "abstract" PageElements and content_object is the "concrete PageElement model/implementation".
Easy to retrieve all elements for a specific page and allows inspection of the ContentType to check the type of element etc.
Just one way of many to solve this particular problem.
To establish the relationship between Page and PageElement in Django you would rather use Foreign Key relationship, than inheritance.
class PageImageElement(PageElement):
page = models.ForeignKey(Page,
on_delete=models.CASCADE,
related_name='images')
image = models.ImageField(null=True)
image_url = models.URLField(null=True)
Every user's post would create an instance of Page. Every addition of image to the Page would create an instance of PageImageElement and you could query for them using the related name. This way would be really easy to access all video, image, text modules of a single Page.
On a related note, I would say that PageElement class could be abstract see the docs and if you declare fields as possibly containing null values as in video = models.FileField(null=True) then it might be worth declaring blank=True as well, otherwise there will be errors when creating the object with these fields undefined. Discussed, for example, here: differentiate null=True, blank=True in django
I can't just write "page.elements" to get all elements of all types
Well actually, you can if you use multi-table inheritance. The problem is that all records returned are instances of PageElement, meaning you lose all information of the subclass type and the additional data these child objects may hold.
There are quite a lot of packages that tackle this polymorphism problem:
django packages: Model inheritance
I am making a django project with models.py having following code:
class Record(models.Model):
id = models.AutoField(primary_key=True)
class TTAMRecord(Record):
client = models.ForeignKey(TTAMClient)
class TTAMClient(models.Model):
...
class Account(models.Model):
records = models.ManyToManyField(Record)
...
I also have following code to insert a TTAMRecord into the records of an Account:
account = Account.objects.get(...)
client = TTAMClient.objects.create(...)
record = TTAMRecord.objects.create(..., client = client, ...)
account.records.add(record)
What I want to do(but can't) is to call the client within the record object of an account; e.g.:
account = Account.objects.get(...)
for record in account.records.all():
client = record.client
...
However, if I am not allowed to do this, since record here is stored as a Record (which doesn't have client) type instead of TTAMRecord(which has client) type...
Any idea how to cast the object?
I want to use the more generic Record instead of TTAMRecord for some purposes not stated here...
As Record is not abstract model, it has its own table and life cycle as other models. However, you can access the corresponding client object as record.ttamclient, so you can change your line to
account = Account.objects.get(...)
for record in account.records.all():
client = record.ttamclient
...
However, this is little cumbersome if you have multiple derived classes. In such cases you would have to know which derived class you are referring to and use the corresponding attribute name.
If I understood your concept of "cast" it will not work in the way you described.
However, to get model inheritance to work you need to use abstract models (see docs)
class Record(models.Model):
# Some generic fields
class Meta:
abstract = True
class TTAMRecord(Record):
client = models.ForeignKey(TTAMClient)
If you need both Record and TTAMRecord to be stored in Account you will need to use polymorphic relationships, that in Django it's called Generic Relations (see docs)
You will need an intermediary model to store these generic relations. So, basically you will have a AccountRecord and a Account model:
class AccountRecord(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
class Account(models.Model):
records = models.ManyToManyField(AccountRecord)
And so you can do:
account = Account.objects.get(...)
for record in account.records.all():
record_content = record.content_object
if isinstance(record_content, TTAMRecord):
client = record_content.client
else:
# No client available