Need help understanding many and source fields in a serializer - django

I am currently trying to familiarize myself with DRF and while going through a tutorial these serializers were used
class EmbeddedAnswerSerializer(serializers.ModelSerializer):
votes = serializers.IntegerField(read_only=True)
class Meta:
model = Answer
fields = ('id', 'text', 'votes',)
class QuestionSerializer(serializers.ModelSerializer):
answers = EmbeddedAnswerSerializer(many=True,source='answer_set')
class Meta:
model = Question
fields = ('id', 'answers', 'created_at', 'text', 'user_id',)
These are the models
class Question(models.Model):
user_id = models.CharField(max_length=36)
text = models.CharField(max_length=140)
created_at = models.DateTimeField(auto_now_add=True)
class Answer(models.Model):
question = models.ForeignKey(Question,on_delete=models.PROTECT)
text = models.CharField(max_length=25)
votes = models.IntegerField(default=0)
My question is in the statement in the Question serializer
answers = EmbeddedAnswerSerializer(many=True,source='answer_set')
what is the purpose of many = True and source='answer_set' ?
I read from the documentation the following regarding many=True
You can also still use the many=True argument to serializer classes.
It's worth noting that many=True argument transparently creates a
ListSerializer instance, allowing the validation logic for list and
non-list data to be cleanly separated in the REST framework codebase.
I am confused by what that means? If I remove many=True from the code I get the error
AttributeError at /api/quest/1/2/
Got AttributeError when attempting to get a value for field `text` on serializer `EmbeddedAnswerSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `RelatedManager` instance.
Original exception text was: 'RelatedManager' object has no attribute 'text'.
Can anyone explain what many=True does and what source field does?

Adding to the answer above by #neverwalkaloner
Many = True
many=True signals that there is more than one object (an iterable) being passed to the serializer. Passing this field in turn triggers the many_init within BaseSerializer to automagically create a ListSerializer instance.
Source Code Snippet:
def __new__(cls, *args, **kwargs):
# We override this method in order to automagically create
# `ListSerializer` classes instead when `many=True` is set.
if kwargs.pop('many', False):
return cls.many_init(*args, **kwargs)
return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
Source = "xyz"
This tells DRF which object attribute supplies the value for the field. The default assumption is that the field name declared on the serializer is the same as the field on the object instance that supplies the value. In cases where this is not true, source allows you to explicitly supply the object instance where the serializer will look for the value. Here's a peek into the def bind(self, field_name, parent) inside serializers.fields where this happens
Source Code Snippet:
# self.source should default to being the same as the field name.
if self.source is None:
self.source = field_name
# self.source_attrs is a list of attributes that need to be looked up
# when serializing the instance, or populating the validated data.
if self.source == '*':
self.source_attrs = []
else:
self.source_attrs = self.source.split('.')
Finally the value is gotten as follows using the source and source_attrs declared in bind:
def get_attribute(self, instance):
"""
Given the *outgoing* object instance, return the primitive value
that should be used for this field.
"""
try:
return get_attribute(instance, self.source_attrs)
except (KeyError, AttributeError) as exc:
Assuming a Question can have multiple Answers, your approach is correct.
The problem appears to be that the source you supplied is the RelatedManager instance itself, and not the queryset of Answer objects. I assumed DRF resolves this accurately, can you try changing it to source='answer_set.all'.
answer_set is the default RelatedManager name given by Django. It might be wise to name your backward relationship using related_name in the Django model. This can be achieved by changing:
question = models.ForeignKey(Question,on_delete=models.PROTECT, related_name='answers')

Probably not the best explanation and someone can add more details but briefly many=True tells to serializer that it will take list of objects for serilizing proccess. In other words it's just a trigger that allows you to specify will you serialize, well, many objects at once, or just single object.
source on the other side specify which attribute of objects should be serializing with current serializer's field.
In practice this line
answers = EmbeddedAnswerSerializer(many=True, source='answer_set')
means that you want to serialize answer_set attribute of Question object with EmbeddedAnswerSerializer. Since answer_set is list of object you should add many=True as argument to make serializer aware that it will work with list of objects instead of single object.

Related

Django one to many saving model is giving error

I have an exam model and I am trying to add a form to add multiple Quiz instance in the parent model. I am getting the following error
raise ValueError(
ValueError: Cannot assign "": "exam_quiz.quiz" must be a "Quiz" instance.
class ExamQuizAdminForm(forms.ModelForm):
class Meta:
model = exam_quiz
exclude = ['registrationDate','examdate']
quiz = forms.ModelMultipleChoiceField(
queryset=Quiz.objects.all(),
required=False,
label=_("Quiz"),
widget=FilteredSelectMultiple(
verbose_name=_("Quiz"),
is_stacked=False))
def __init__(self, *args, **kwargs):
super(ExamQuizAdminForm, self).__init__(*args, **kwargs)
if self.instance.pk:
self.fields['quiz'].initial = \
self.instance.quiz
def save(self, commit=True):
exam_quiz = super(ExamQuizAdminForm, self).save(commit=False)
exam_quiz.save()
exam_quiz.quiz_set.set(self.cleaned_data['Quiz'])
self.save_m2m()
return exam_quiz
class ExamQuizAdmin(admin.ModelAdmin):
form = ExamQuizAdminForm
list_display = ('examname','month','year')
Assuming exam_quiz.quiz is a m2m field...
I don't think you need to override the save function in this case. Use the save_m2m() function if you want to, say, add additional non form data, and then save both the normal and m2m data from the form. According to the docs
"Calling save_m2m() is only required if you use save(commit=False).
When you use a save() on a form, all data – including many-to-many
data – is saved without the need for any additional method calls."
Here, however, it looks like the only change you're making is adding the m2m data, which the normal save can handle.
Also, you might try making the Quiz lowercase, eg,
exam_quiz.quiz_set.set(self.cleaned_data['quiz'])
as I don't think 'label' or 'verbose_name' affect the HTML field name.

Model Method from rest_framework modelSerializer

this is a simple question but I'm very new to django-rest-framework.
I was wondering if there is any way to access a method defined on the model from the serializer.?
Say I have a model
class Listing(models.Model):
listingid = models.BigIntegerField(primary_key=True)
mappingid = models.BigIntegerField()
projectlevelid = models.IntegerField()
subsellerid = models.IntegerField()
iscreatedbyadmin = models.BooleanField(default=None, null=True)
createdon = models.DateTimeField(auto_now_add=True, editable=False)
validationstatus = models.SmallIntegerField(default=0)
def is_project(self):
""" Returns True if listing is of Project Type (projectlevelid=6) else False"""
if self.projectlevelid == 6:
return True
else:
return False
def get_project_info(self):
"""Returns False if listing is not mapped to a project, else returns the project info"""
if self.is_project() == False:
return False
return models.Project.objects.get(projectid=self.mappingid)
Is it possible for the serializer
class ListingSerializer(serializers.ModelSerializer):
class Meta:
model = models.MasterListing
to have access to Listing.is_project i.e. for an object of the Listing model, can the serializer call its is_project method?
If so, can I set a field in the serializer such that if is_project returns true, the field is populated?
I am trying for something like this,
class ListingSerializer(serializers.ModelSerializer):
project = serializers.SomeRELATEDFieldTYPE() # this field if populated if the `is_project` is true
class Meta:
model = models.MasterListing
I understand I can do this using some combination of required=False and SerializerMethodField, but maybe there is a simpler way?.
Note: It is not possible for me to set a foreign key to the mappingid, since it depends on the projectlevelid. I also can't affect this relationship so no further normalization is possible. I know that there might be some way using content-types, but we are trying to avoid that if it is possible..
EDIT: I solved the problem, but not as the question specified.
I used this:
class ListingSerializer(serializers.ModelSerializer):
project = serializers.SerializerMethodField()
def get_project(self, obj):
"""Returns False if listing is not mapped to a project, else returns the project info"""
if str(obj.projectlevelid) == str(6):
projectObj = models.Project(projectid=obj.mappingid)
projectObjSerialized = ProjectSerializer(projectObj)
return projectObjSerialized.data
return False
class Meta:
model = models.MasterListing
So, the original question still stands: "Is it possible for the modelSerializer to access its models methods?"
Also, another problem that now appears is, can I make the serializer exclude fields on demand, i.e. can it exclude mappingid and projectlevelid if it is indeed a project?
For your first question source attribute is the answer, citing:
May be a method that only takes a self argument, such as
URLField('get_absolute_url')
For your second answer, yes it is also possible. Check the example it provides in their docs: http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields
PS: I really love drf for its very complete documentation =).
EDIT
To use the source attribute you can just declare a new explicit field like so:
is_project = serializers.BooleanField(source='is_project')
With this, is_project field has the value of the is_project method of your instance. Having this, when creating the dynamic serializer (by modifying its init method) you can add the 'project' field if it's True.
#argaen is absolutely right, source is a DRF core argument, and would most definitely solve your problem. However, it's redundant to use source, if the field name is the same as the source. So the above answer won't require you specify source, since field name is_project is the same as source name is_project.
So in your case:
is_project = serializers.BooleanField()

Django Rest Framework, relationship between a Custom Serializer and an Id field

Given a serializer with a reference to a custom serializer:
class IndustryIdeaSerializer(serializers.ModelSerializer):
sub_industry = IndustrySerializer(many=False, read_only=True)
class Meta:
model = myModels.IdeaIndustry
fields = (
'id'
, 'sub_industry'
)
I am unable to save changes to this class when I post JSON like { sub_industry: 12 } or { sub_industry_id: 12 }
It does return the right JSON for displaying the data, and I wouldn't change it from that perspective. However changing it to:
class IndustryIdeaSerializer(serializers.ModelSerializer):
class Meta:
model = myModels.IdeaIndustry
fields = (
'id'
, 'sub_industry'
)
Gives me the save action (can persist with the simple JSON) I want BUT not the read action (doesn't return all the data associated with that foreign key)!
First am I missing something obvious? Is there a pattern to deal with behavior I am after - namely read and return the deep tree, but persist with just the Id's
This is for DRF 3.0. I just whipped this up this afternoon, I will follow up if I encounter any unforeseen problems (likewise, let me know if you spot anything wrong! I am fairly new to DRF)
class EnhancedPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
'''
This custom field extends the PrimaryKeyRelatedField
It overrides to_representation (which generates the data to be
serialized) to use a given serializer.
This allows other serializers to show nested data about a related
field, while still allowing the client to set relations by simply
passing an id.
To initialize, pass the queryset and serializer arguments.
The serializer argument should be a Serializer class.
If the serializer provides Meta.model (such as a ModelSerializer),
and you wish to use the queryset provided by that serializer, you may
omit the queryset argument.
e.g.
# without queryset
child_object = EnhancedPrimaryKeyRelatedField(
serializer=ChildObjectSerializer
)
# with queryset
child_object = EnhancedPrimaryKeyRelatedField(
queryset=models.ChildObject.objects.all(),
serializer=SomeSpecializedSerializer
)
'''
def __init__(self, *args, **kwargs):
assert 'serializer' in kwargs
self.serializer = kwargs['serializer']
del kwargs['serializer']
if 'queryset' not in kwargs:
# Catch any programmer errors
assert 'Meta' in self.serializer.__dict__
assert 'model' in self.serializer.Meta.__dict__
kwargs['queryset'] = self.serializer.Meta.model.objects.all()
super(serializers.PrimaryKeyRelatedField, self).__init__(*args, **kwargs)
def to_representation(self, data):
if hasattr(data.pk, 'all'): # are we dealing with a collection?
return self.serializer(data.pk.all(), many=True).data
elif hasattr(data, 'pk') and data.pk:
return self.serializer(self.queryset.get(pk=data.pk)).data
else:
return data.pk
There's nothing built in that handles this explicitly, but it's now come up a couple of times recently (e.g. here so perhaps we need to make it easier.
The work-around is to subclass PrimaryKeyRelatedField, which will handle setting the relation and override to_native to provide the full serialisation you're after.
I hope that helps.

django-rest-framework - trying to set required=False flag on nested 1-to-M?

I'm having some issue with django-rest-framework, and nested objects.
I have a Cart object, as well as CartItem, which links back to a Cart:
class Cart(models.Model):
customer = models.ForeignKey(Customer)
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
class CartItem(models.Model):
cart = models.ForeignKey(Cart, related_name='cartitems')
product = models.ForeignKey(Product, help_text='Product in a cart')
quantity = models.PositiveIntegerField(default=1, help_text='Quantity of this product.')
date_added = models.DateTimeField(auto_now_add=True, help_text='Date that this product was added to the cart.')
I've created serializers for both:
class CartItemSerializer(serializers.ModelSerializer):
product = serializers.HyperlinkedRelatedField(view_name='product-detail')
class Meta:
model = CartItem
class CartSerializer(serializers.ModelSerializer):
customer = serializers.HyperlinkedRelatedField(view_name='customer-detail')
cartitems = CartItemSerializer(required=False)
total_price = serializers.CharField(source='total_price', read_only=True)
shipping_cost = serializers.CharField(source='shipping_cost', read_only=True)
class Meta:
model = Cart
fields = ('id', 'customer', 'date_created', 'date_modified', 'cartitems', 'total_price', 'shipping_cost')
However, whenever I try to POST to create a new cart, I get an error, assumedly when it tries to set the non-existent CartItem:
TypeError at /api/v1/carts/
add() argument after * must be a sequence, not NoneType
However, a Cart isn't required to actually have CartItems.
Is there any way to get DRF to respect the required=False flag I get on Cart.cartitems?
Cheers,
Victor
EDIT:
I took a stab at tracing it through again:
It's calling BaseSerializer.save() in rest_framework/serializers.py with a CartSerializer object.
def save(self, **kwargs):
"""
Save the deserialized object and return it.
"""
if isinstance(self.object, list):
[self.save_object(item, **kwargs) for item in self.object]
if self.object._deleted:
[self.delete_object(item) for item in self.object._deleted]
else:
self.save_object(self.object, **kwargs)
return self.object
It then calls save_object() on the same class:
def save_object(self, obj, **kwargs):
"""
Save the deserialized object and return it.
"""
if getattr(obj, '_nested_forward_relations', None):
# Nested relationships need to be saved before we can save the
# parent instance.
for field_name, sub_object in obj._nested_forward_relations.items():
if sub_object:
self.save_object(sub_object)
setattr(obj, field_name, sub_object)
obj.save(**kwargs)
if getattr(obj, '_m2m_data', None):
for accessor_name, object_list in obj._m2m_data.items():
setattr(obj, accessor_name, object_list)
del(obj._m2m_data)
if getattr(obj, '_related_data', None):
for accessor_name, related in obj._related_data.items():
if isinstance(related, RelationsList):
# Nested reverse fk relationship
for related_item in related:
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
setattr(related_item, fk_field, obj)
self.save_object(related_item)
# Delete any removed objects
if related._deleted:
[self.delete_object(item) for item in related._deleted]
elif isinstance(related, models.Model):
# Nested reverse one-one relationship
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
setattr(related, fk_field, obj)
self.save_object(related)
else:
# Reverse FK or reverse one-one
setattr(obj, accessor_name, related)
del(obj._related_data)
The Cart object has a _related_data field that is set to a dict:
{'cartitems': None}
Hence, on the second-last line, it calls setattr in django/db/models/fields/related.py:
def __set__(self, instance, value):
if instance is None:
raise AttributeError("Manager must be accessed via instance")
manager = self.__get__(instance)
# If the foreign key can support nulls, then completely clear the related set.
# Otherwise, just move the named objects into the set.
if self.related.field.null:
manager.clear()
manager.add(*value)
It's this last liner (manager.add(*value)) that causes the:
TypeError: add() argument after * must be a sequence, not NoneType
Checking the Serializer Relation Docs, first you need to add many=True to your cartitems field.
Unfortunately this is read-only. The docs just say "For read-write relationships, you should use a flat relational style" — you can find a question about that here (although that's only dealing with the 1-1 case).
Current strategies involve making cartitems read-only and then either: doing something post_save, using a second serializer or making a separate request to a separate endpoint to set the related entities. Given that better support for Nested Writes is coming I'd probably be inclined towards a separate request to a separate endpoint for the moment (though that will obviously depend on your constraints).
I hope that helps.
EDIT: (After update to question & discussion in comments).
If you're using a separate endpoint for adding CartItems then making cartitems read-only should eliminate the error.
However (if you're not making it read-only) looking at the DRF code you posted from save_object it occurs that in the related_item in related block you really do need a list. The appropriate dict (fragment) for a Cart with no CartItems is not {'cartitems': None} but rather {'cartitems': []}. — This of course means your required=False flag isn't doing anything. (So perhaps the short answer is "No" — Will now defer to the mailing list discussion

Access model instance from model field

I want my Django custom model field to set an attribute on the model instance.
I'm sure it's not working this way but here is an example:
class MyField(models.Field):
__metaclass__ = models.SubfieldBase
def __init__(self, *args, **kwargs):
super(MyField, self).__init__(*args, **kwargs)
model_instance = ????
setattr(model_instance, "extra_attribute", "It's working!")
class MyModel(models.Model):
my_field = MyField()
model_instance = MyModel.objects.get(pk=123)
print model_instance.extra_attribute # output: "It's working!"
Django's ForeignKey model field is doing a similar thing, so it is possible :P
I think ForeignKey field is using the contribute_to_class method.
You can create a special Proxy class that will replace the field. When getting or setting field value, you can use 'obj' attribute to access model instance. See this example:
class ObjectField(models.PositiveSmallIntegerField):
class ObjectProxy:
def __init__(self, field):
self.field = field
def __get__(self, obj, model):
if obj is None:
return self.field # this is required for initializing model field
value = obj.__dict__[self.field.name] # get actual field value
# ... here you can do something with value and model instance ("obj")
return value
def __set__(self, obj, value):
# same here
obj.__dict__[self.field.name] = value
def contribute_to_class(self, cls, name):
super().contribute_to_class(cls, name)
# set up our proxy instead of usual field
setattr(cls, name, ObjectField.ObjectProxy(self))
You do not have access to the model instance from inside your Field object, sorry. Django's ForeignKey accomplishes the foo_id thing by having separate name and attname fields, but the actual setting of foo_id = 123 is done the same way as all the other model fields, deep in the QuerySet code, without interacting with the field classes.
And conceptually, what you're trying to do is a bad idea - action-at-a-distance. What if adding a particular field could cause bugs in unrelated model functionality, say, if an attribute another field was expecting got overridden? It would be difficult to debug, to say the least. I don't know what your underlying goal is, but it should probably be done in model code, not a field class.
Here's a ModelField that does what you want:
https://gist.github.com/1987190
That's actually pretty old (like maybe pre-1.0, don't remember now), had to dust it off a bit - I'm not sure if it still works. But it's definitely doable, hopefullly this gives you an idea.
init is called when Django processes the Model Class, not the Model Instance. So, you can add the attribute to the Model Class (e.g. by using 'add_to_class' http://www.alrond.com/en/2008/may/03/monkey-patching-in-django/ ). To add the attribute to the instance you should override the init of the instance (but I think this is not your case).
How about
model_instance = SomeExtraModel.objects.get(pk=1456)
replacing 1456 with something that makes sense