Django Rest Framework: Get field name from model definition - python

Within the Django Rest framework documentation it is suggested to declare the "field" list explicitly to avoid providing the data of new columns just by adding them to the model which may contain sensitive information.
The field list is an array of strings, containing the field ids. To avoid declaring field ids, which actually do not exist in the model (e.g. typos or changed models) I tried to declare the list using object references - but always end up with "DeferredAttribute: object has no attribute ".
I have read something that meta information is not available in objects and that you could solve that by defininig your own Meta class using Object._meta.get_fields() and store it in the class, but I thought there might be a simpler/more elegant way (and I do now know, how, in detail ;-)).
Example:
class Samples(models.Model):
# Meta data, primarily used in AdminSite.
class Meta:
verbose_name = _('Samples')
verbose_name_plural = _('Samples')
samples_boolfield = models.BooleanField
samples_textfield = models.CharField(max_length=2000, blank=True)
views.py:
class SamplesView(viewsets.ModelViewSet):
serializer_class = SamplesSerializer
queryset = Samples.objects.all()
serializers.py:
Version 1, which does not show any errors in pyCharm or makemigrations, but calling the API reults in "TypeError at /api/samples/: argument of type 'DeferredAttribute' is not iterable":
class SamplesSerializer(serializers.ModelSerializer):
class Meta:
model = Samples
fields = (
'id',
Samples.samples_boolfield,
Samples.samples_textfield,
)
Version 2, which does not show any errors in pyCharm, but makemigrations fails with "DeferredAttribute: object has no attribute name":
class SamplesSerializer(serializers.ModelSerializer):
class Meta:
model = Samples
fields = (
'id',
Samples.samples_boolfield.__name__,
Samples.samples_textfield.__name__,
)
Version 3, which does not show any errors in pyCharm, but makemigrations fails with "DeferredAttribute: object has no attribute get_attname":
class SamplesSerializer(serializers.ModelSerializer):
class Meta:
model = Samples
fields = (
'id',
Samples.samples_boolfield.get_attname(),
Samples.samples_textfield.get_attname(),
)
Is there a way to declare the field list using object references (so that it fails e.g. in pyCharm/during compilation)?
Thank you for your feedback.
Regards,
HerrB92

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-parler - M2M Relationship Issue with Admin M2M Field (Circular Dependency?)

we're facing an issue with the use of M2M relationships - It seems we have a circular dependency issue?
We need to make a M2M relationship to a TranslatableModel field (which is M2M under the hood), exposed in the Admin (we have a multi-select widget that utilises the M2M field).
Lots of research has only revealed this specific debug info (thanks to #egasimus):
models.py
# Here's a pretty basic model with a translatable M2M field.
class ContentItem(TranslatableModel):
translations = TranslatedFields(
title = models.CharField(max_length=200),
content = models.TextField(blank=True),
primary_media = models.ForeignKey(
'media.MediaAsset', related_name='cards_where_primary',
blank=True, null=True)
extra_media = models.ManyToManyField(
'media.MediaAsset', related_name='cards_where_extra',
blank=True, null=True))
author = models.ForeignKey(settings.AUTH_USER_MODEL)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
admin.py
class ContentItemAdmin(TranslatableAdmin):
fields = ('title', 'content', 'primary_media', 'extra_media',
'author', 'created', 'modified')
# The above code results in the following error:
# FieldError at /admin/cms/contentitem/1/
# Unknown field(s) (extra_media) specified for ContentItem. Check fields/fieldsets/exclude attributes of class CardAdmin.
class ContentItemAdmin(TranslatableAdmin): pass
# And, if I don't explicitly define fields, the `extra_media` field doesn't show up at all.
# This is confirmed if I run `manage.py shell` and I retrieve a ContentItem instance:
# it simply does not have an `extra_media` attribute. However, if I do manually retrieve
# a translation instance, the `extra_media` M2M field is there - it just doesn't end up
# getting added to the shared object.
models.py
# Adding a call to `get_m2m_with_model()`, though, makes any translated M2M fields
# appear on the shared object, and in the admin.
class TranslatedFieldsModel(models.Model):
def _get_field_values(self):
return [getattr(self, field.get_attname()) for field, _
in self._meta.get_fields_with_model()
+ tuple(self._meta.get_m2m_with_model())]
#classmethod
def get_translated_fields(cls):
return [f.name for f, _
in cls._meta.get_fields_with_model()
+ tuple(cls._meta.get_m2m_with_model())
if f.name not in ('language_code', 'master', 'id')]
# However, when I try to save a new ContentItem via the admin,
# I get the aforementioned error:
#
# "<ContentItemTranslation: #None, bg, master: #None>" needs to have a value for field
# "contentitemtranslation" before this many-to-many relationship can be used.
#
# I assume that this has to do with the order in which the shared model, the translation model,
# and the M2M intermediate model interact, and the order in which they are saved.
Has anyone faced such an issue? Any pointers to how we can get around this problem?
Thank you for any help you can provide.

Expose multiple similar database fields as enumerable collection

I have a Django (1.8) Model for an underlying database table that has multiple columns that are logically a fixed-size array. For example:
from django.db import models
class Widget(models.Model):
# ...
description_1 = models.CharField(max_length=255)
description_2 = models.CharField(max_length=255)
description_3 = models.CharField(max_length=255)
# ...
I would like to be able to access these columns as if they were a collection on the model instance, e.g.:
instance = Widget.objects.get(...)
for description in instance.descriptions:
# do something with each description
My primary motivation is that I am exposing this model via Django Rest Framework (DRF), and would like the API clients to be able to easily enumerate the descriptions associated with the model. As it stands, the clients have to reference each logical 'index' manually, which makes the code repetitive.
My DRF serializer code is currently like this:
class WidgetSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Widget
There are a fixed number of descriptions for each Widget, and their ordering is important.
Is there a clean way to expose these fields as a collection on the Model object?
It really was as easy as adding a method to the Model class that returns the fields as a sequence, and then (for API clients), manually specifying that new method as a field to serialize.
So the Model definition becomes:
from django.db import models
class Widget(models.Model):
description_1 = models.CharField(max_length=255)
description_2 = models.CharField(max_length=255)
description_3 = models.CharField(max_length=255)
def descriptions(self):
return self.description_1, self.description_2, self.description_3
And the DRF serializer is updated like:
class WidgetSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Widget
fields = ('url', 'descriptions',)
This causes the API to return a JSON array for descriptions and omit all of the individual description_x fields.

Multiple Django Admin Arguments with Extensions

Is there a way to use multiple Django extensions in the admin.site.register() inside admin.py? I'm using "simple-history" and "import-export" extensions, but I can only have one of them in the admin.site.register().
Example: I have a model named, "Cars", that is using the "simple-history" extension so I need admin.site.register(Cars, SimpleHistoryAdmin), as their documentation says it should. I want to use the import/export extension as well to the same "Cars" model, but the admin.site.register() doesn't take multiple arguments for me to add it.
models.py
class Cars(models.Model):
Year = models.CharField(max_length=30)
Make = models.CharField(max_length=30)
Model = models.CharField(max_length=30)
history = HistoricalRecords()
class Meta:
verbose_name_plural = "Car Table"
def __str__(self):
return self.Make
admin.py
class CarResource(resources.ModelResource):
class Meta:
model = Cars
fields = ('id','Year', 'Make', 'Model',)
class CarAdmin(ImportExportModelAdmin):
resource_class = CarResource
pass
#I want to use the import/export extension (code above), along with simple-history
admin.site.register(Cars, CarAdmin)
admin.site.register(Cars, SimpleHistoryAdmin)
I've tried using a proxy and inlines, but the proxy makes a new model which I don't want and when using inlines I get an error saying that it requires a foreign key, but I'm not trying to get the model objects from a different model. Naming them the same model doesn't work because the model is already registered. Any help is much appreciated!
In python, class can have more than one parent. Just inherit from 2 parents at once. But both ImportExportModelAdmin and SimpleHistoryAdmin are inheriting from ModelAdmin, that's not good. There is also ImportExportMixin, we can use it instead of ImportExportModelAdmin, so there will be only one reference to ModelAdmin.
class CarResource(resources.ModelResource):
class Meta:
model = Cars
fields = ('id','Year', 'Make', 'Model',)
class CarAdmin(ImportExportMixin, SimpleHistoryAdmin):
resource_class = CarResource
pass
#I want to use the import/export extension (code above), along with simple-history
admin.site.register(Cars, CarAdmin)

Django REST Framework serializer field required=false

from the documentation:
read_only
Set this to True to ensure that the field is used when serializing a representation, but is not used when updating an instance during deserialization.
Defaults to False
required
Normally an error will be raised if a field is not supplied during deserialization. Set to false if this field is not required to be present during deserialization.
Defaults to True.
So I have a model which has a field that's not nullable but I want it to be populated in the pre_save method, so I have set the field to required=False in serializer, but doesn't seem to work. I am still getting error when saving the record.
class FavoriteListSerializer(serializers.ModelSerializer):
owner = serializers.IntegerField(required=False)
class Meta:
model = models.FavoriteList
Update:
I have added serializer_class = serializers.FavoriteListSerializer to the ViewSet, now instead of getting This field is required, which I think got past the validation but then I am getting This field cannot be null. I have checked the pre_save method is not being executed, any ideas?
Yeah, I ran into this issue at some point as well. You need to also update the validation exclusions.
class FavoriteListSerializer(serializers.ModelSerializer):
owner = serializers.IntegerField(required=False)
class Meta:
model = models.FavoriteList
def get_validation_exclusions(self):
exclusions = super(FavoriteListSerializer, self).get_validation_exclusions()
return exclusions + ['owner']
Late Entry to this thread. This issue was fixed in django-rest-framework 2.3.13. Here is the link of the PR.
You use it like this in your case:
class Meta:
model = models.FavoriteList
optional_fields = ['owner', ]
In case somebody lands here with a similar issue, pay attention to the following attributes along with required:
allow_blank:
If set to True then the empty string should be considered a valid value.
allow_null:
Normally an error will be raised if None is passed to a serializer field.
required:
Normally an error will be raised if a field is not supplied during deserialization.
I was straggling to figure out why I was getting a validation error with required=False where I had missed the allow_null attribute.
In 2020, for DRF 3.12.x, the approach that I prefer the approach that relies on
Serializer's extra_kwargs.
So assuming your
class FavoriteListSerializer(serializers.ModelSerializer):
owner = serializers.IntegerField(required=False)
class Meta:
model = models.FavoriteList
fields = ["owner"] # and whatever other fields you want to expose
extra_kwargs = {"owner": {"required": False, "allow_null": True}}
If you have unique_together constraint on one of the fields you are trying to set required=False you need to set validators=[] in serializers Meta like
class FavoriteListSerializer(serializers.ModelSerializer):
owner = serializers.IntegerField(required=False)
class Meta:
model = models.FavoriteList
validators = []
Here is the original answer
You can also do this:
class ASerializer(serializers.HyperlinkedModelSerializer):
owner = serializers.HiddenField(default=serializers.CurrentUserDefault())
...
As referred here: https://www.django-rest-framework.org/api-guide/validators/#advanced-field-defaults
There you can also find the case when you also wanna let the view show owner
I would set model field to allow null value (and possible also default to None)
class FavoriteList(models.Model):
owner = models.PositiveIntegerField(null=True, default=None)
Then it's possible to just leave owner field to Meta section. These fields, without any extra settings, will automatically get all attributes from model field and be non-required.
class FavoriteListSerializer(serializers.ModelSerializer):
class Meta:
model = models.FavoriteList
fields = ('owner',)

Categories

Resources