Passing partial=True down to nested serializer in DRF - python

I have two serializers organised like this:
class OuterSerializer():
inner_obj = InnerSerializer(many=True, required=False)
other fields ......
class InnerSerializer():
field_1 = CharField()
field_2 = CharField()
Now my use case is to partial update the outer serializer's model. How I'm doing that is:
def partial_update(self, request, *args, **kwargs):
serializer = OuterSerializer(data=request.data, context={'request': self.request}, partial=True)
serializer.is_valid(raise_exception=True)
data = serializer.data
outerobj = self.service_layer.update(kwargs['pk'], data, request.user)
response_serializer = OpportunitySerializer(instance=outerobj, context={'request': self.request})
return Response(response_serializer.data, HTTPStatus.OK)
The issue is this partial flag does not get passed down to the InnerSerializer.
For example if my request body looks like below, I want it to work:
{"inner_obj":
{
"field_1" : "abc"
}
}
Currently I get a 400 error for this saying the field is required.
What I've tried :
Setting the partial variable within the OuterSerializer in the init method by modifying it as such
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# We pass the "current serializer" context to the "nested one"
self.fields['inner_obj'].context.update(self.context)
self.fields['inner_obj'].partial = kwargs.get('partial')
However this doesn't travel down.

Try to modify the InnerSerializer so that it could accept the partial argument and pass it to its parent, like following:
class InnerSerializer(serializers.Serializer):
field_1 = CharField()
field_2 = CharField()
def __init__(self, *args, **kwargs):
self.partial = kwargs.pop('partial', False)
super().__init__(*args, **kwargs)
class OuterSerializer(serializers.Serializer):
inner_obj = InnerSerializer(many=True, required=False)
other fields ......
def __init__(self, *args, **kwargs):
partial = kwargs.get('partial')
super().__init__(*args, **kwargs)
self.fields['inner_obj'].child.partial = partial
Another possible solution.
You can also override the to_internal_value() method in the InnerSerializer to make it accept partial updates so:
class InnerSerializer(serializers.Serializer):
field_1 = CharField()
field_2 = CharField()
def to_internal_value(self, data):
if self.partial:
return {field: data.get(field, getattr(self.instance, field)) for field in data}
return super().to_internal_value(data)
class OuterSerializer(serializers.Serializer):
inner_obj = InnerSerializer(many=True, required=False)
other fields ......
Edit:
For the error:
KeyError: "Got KeyError when attempting to get a value for field field_2on serializerInnerSerializer`.
The error message you're encountering suggests that the serializer is trying to access the value for field_2 from the data, but it's not present.
Currently to solve the error, you should override the to_representation() method in the InnerSerializer to only include the fields that are present so:
class InnerSerializer(serializers.Serializer):
field_1 = CharField()
field_2 = CharField()
def to_representation(self, instance):
data = super().to_representation(instance)
return {field: value for field, value in data.items() if value is not None}

Related

Dajngo admin ModelForm: how to get request and/or user loggin in my form to filter queryset?

I have a custom User class with a property that return a queryset
And I have an Admin class that use a custom ModelForm with 2 ModelChoiceField and fone BooleanFields.
I want to filter queryset of one ModelChoiceField using user property
but my problem is that I do not have access to request or user in my ModelForm.
I try to use method get_form_kwargs I use for 'normal CBV' but it doen't work as this method do not exist in ModelAdmin
admin.py
class User_TableAdmin(SimpleHistoryAdmin):
def __init__(self, model, admin_site):
super(User_TableAdmin,self).__init__(model,admin_site)
self.form.admin_site = admin_site # capture the admin_site
form = User_TableAdminForm **# How to request object to my form?**
list_display = ('id','user','table','can_download')
search_fields = ('user','table','can_download')
forms.py
class User_TableAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(User_TableAdminForm, self).__init__(*args, **kwargs)
# add the 'green +' button to create a new user (green + button suppress when overidding field with ModelChoiceField)
self.fields['user'].widget = RelatedFieldWidgetWrapper(
self.fields['user'].widget,
self.instance._meta.get_field('user').remote_field,
admin_site)
class Meta:
model = User_Table
fields = '__all__'
# display only tables of study database that user workin: User property
# tables = self.user.can_download
tables = Table.objects.all() **#<- I would like to use something like request.user.can_download**
user = forms.ModelChoiceField(queryset = User.objects.all(), label = "User", widget = forms.Select())
table = forms.ModelChoiceField(queryset = tables, label = "Table", widget = forms.Select())
can_download = forms.BooleanField(
widget = forms.CheckboxInput(),
required = False,
)
models.py
class User(AbstractUser):
# site = models.ForeignKey(Site, on_delete = models.CASCADE, related_name="database")
birth_date = models.DateField(null=True, blank=True)
#property
def can_download(self):
""" Return the related list of tables use can download. """
return Table.objects.filter(
Q(database__study__in = [uss.study.id for uss in User_Site_Study.objects.filter(user = self.id)]) &
Q(database__study__is_opened = True) &
Q(database__is_opened = True)
)
can_download.fget.short_description = 'List of tables user allowed to download'
For that you can override ModelAdmin.get_form() which returns the ModelForm class that will be used in add or change admin page and decorate it to inject request upon creation of form instance.
class ModelFormWithRequest(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
super().__init__(*args, **kwargs)
#classmethod
def inject_request(cls, request):
def __new__(_, *args, **kwargs):
kwargs.setdefault('request', request)
return cls(*args, **kwargs)
return type(
f'{cls.__name__}Decorator',
(cls,),
{
'__module__': cls.__module__,
'__doc__': cls.__doc__,
'__new__': __new__
}
)
class User_TableAdminForm(ModelFormWithRequest):
def __init__(self, *args, **kwargs):
super(ModelFormWithRequest, self).__init__(*args, **kwargs)
self.fields['table'].queryset = # set new queryset filtered with self.request.user data
# add the 'green +' button to create a new user (green + button suppress when overidding field with ModelChoiceField)
self.fields['user'].widget = RelatedFieldWidgetWrapper(
self.fields['user'].widget,
self.instance._meta.get_field('user').remote_field,
admin_site)
class User_TableAdmin(SimpleHistoryAdmin):
form = User_TableAdminForm
list_display = ('id','user','table','can_download')
search_fields = ('user','table','can_download')
def __init__(self, model, admin_site):
super(User_TableAdmin,self).__init__(model,admin_site)
self.form.admin_site = admin_site # capture the admin_site
# this is how you pass request to form
def get_form(self, request, obj=None, change=False, **kwargs):
ModelForm = super().get_form(request, obj=obj, change=change, **kwargs))
# pass request only to change form, it assumes this is a subclass of auth.UserAdmin
if change:
return ModelForm.inject_request(request)
return ModelForm
just to extend answer, if one needs to pass request to InlineModelAdmin's forms, which is handled by formsets, need to override get_formset() instead of get_form()
class CustomInlineModelAdmin(admin.InlineModelAdmin):
def get_formset(self, request, obj=None, **kwargs):
formset_class = super().get_formset(request, obj=obj, **kwargs)
formset_class.form = formset_class.form.inject_request(request)
return formset_class
There is a shorter solution, just set ModelForm.request = request in ModelAdmin.get_form(). In this case ModelFormWithRequest is not needed, but I prefer to pass dynamic dependencies though constructor instead of appending it to class objects which are global.

Django save default value in Proxy Model

Different proxy models should be different in type.
If I query those models I the right ones.
I am trying to save a default type field in a proxy model.
I don't want to set it everytime in the view.
This does not work. The type field is always "TYPE1".
models.py:
class MyModel(models.Model):
class ModelType(models.TextChoices):
TYPE1 = 'TYPE1', _('TYPE1')
TYPE2 = 'TYPE2', _('TYPE2')
type = models.CharField(max_length=100, choices=ModelType.choices, default='TYPE1')
class Type2Manager(models.Manager):
def get_queryset(self):
return super(Type2Manager, self).get_queryset().filter(type='TYPE2')
def save(self, *args, **kwargs):
kwargs.update({'type': 'TYPE2'})
return super(Type2Manager, self).save(*args, **kwargs)
class Type2ProxyModel(MyModel):
class Meta:
proxy = True
objects = Type2Manager()
views.py:
def create_type2_model(request):
form = Type2Form(request.POST, initial={})
f = form.save(commit=False)
f.save()
forms.py:
class Type2Form(ModelForm):
class Meta:
model = Type2ProxyModel
Update 25.02.2020 12:18:
I found out that this sets the correct type. But I don't know how to use create() in a ModelForm.
class Type2Manager(models.Manager):
...
def create(self, **kwargs):
kwargs.update({'type': 'TYPE2'})
return super(Type2Manager, self).create(**kwargs)
Type2ProxyModel.objects.create()
A model manager operates on a "table-level". When you create an object via a form it uses the model objects and not the model manager and thus you'd need to override the save of your proxy model. If I modify your Type2ProxyModel to this it works:
class Type2ProxyModel(MyModel):
class Meta:
proxy = True
objects = Type2Manager()
def save(self, *args, **kwargs):
self.type = 'TYPE2'
return super(Type2ProxyModel, self).save(*args, **kwargs)

Django: Using get_form_kwarg to pass url pramater into form __init__ for ModelChoiceField selection filiter

I am building a FAQ app.
Model flow Topic -> Section -> Article.
Article has a FK to Section which has a FK to Topic.
In my create article from I want to take in the Topic_Pk so when the user selects a Section the choice selection is limited to just the Sections attached under the Topic.
I am using get_from_kwarg to pass the Topic_Pk from the url to __init__ in the form. I keep getting a TypeError __init__() got an unexpected keyword argument 'topic_pk'. I do not want to pop the data or set topic_pk=None in the __init__ parameters as this would invalidate the whole point.
What is it I am missing to allow me to use this variable?
Url:
url(r'^ironfaq/(?P<topic_pk>\d+)/article/create$', ArticleCreateView.as_view()),
View:
class ArticleCreateView(CreateView):
model = Article
form_class = CreateArticleForm
template_name = "faq/form_create.html"
success_url = "/ironfaq"
def get_form_kwargs(self):
kwargs = super(ArticleCreateView,self).get_form_kwargs()
kwargs.update(self.kwargs)
return kwargs
Form:
class CreateArticleForm(forms.ModelForm):
section = forms.ModelChoiceField(queryset=Section.objects.none())
def __init__(self, *args, **kwargs):
super(CreateArticleForm, self).__init__(*args, **kwargs)
self.fields['section'].queryset = Section.objects.filter(topic_pk=self.kwargs['topic_pk'])
class Meta:
model = Article
widgets = {
'answer': forms.Textarea(attrs={'data-provide': 'markdown', 'data-iconlibrary': 'fa'}),
}
fields = ('title','section','answer')
Model:
class Article(Audit):
title = models.CharField(max_length=255)
sort = models.SmallIntegerField()
slug = models.SlugField()
section = models.ForeignKey(Section,on_delete=models.CASCADE)
answer = models.TextField()
vote_up = models.IntegerField()
vote_down = models.IntegerField()
view_count = models.IntegerField(default=0)
class Meta:
verbose_name_plural = "articles"
def __str__(self):
return self.title
def total_votes(self):
return self.vote_up + self.vote_down
def percent_yes(self):
return (float(self.vote_up) / self.total_votes()) * 100
def get_absolute_url(self):
return ('faq-article-detail',(), {'topic__slug': self.section.topic.slug,
'section__slug': self.section.slug, 'slug': self.slug})
For your current __init__ signature, you must pop topic_pk from kwargs before you call super(), otherwise you'll get the TypeError.
In your question, you say that popping the value would 'invalidate the whole point', but I think you're mistaken. You can still use the topic_pk value after calling super().
class CreateArticleForm(forms.ModelForm):
section = forms.ModelChoiceField(queryset=Section.objects.none())
def __init__(self, *args, **kwargs):
topic_pk = kwargs.pop('topic_pk')
super(CreateArticleForm, self).__init__(*args, **kwargs)
self.fields['section'].queryset = Section.objects.filter(topic_pk=topic_pk)
Another approach would be to use topic_pk as a named argument. Note that this changes the signature of the __init__ method, so it might break other code (for example if you had CreateArticleForm(request.POST) somewhere else).
def __init__(self, topic_pk=None, *args, **kwargs):
super(CreateArticleForm, self).__init__(*args, **kwargs)
self.fields['section'].queryset = Section.objects.filter(topic_pk=topic_pk)

How to solve ValueError When save ModelMultipleChoiceField in ModelForm?

My model.py:
class RelayAddress(models.Model):
id = models.AutoField(primary_key=True,default=0)
sister_relay_relation = models.ManyToManyField('self', through='RelaySisterRelation',symmetrical=False)
def save(self, *args, **kwargs):
self.update_time = int(time.time())
super(RelayAddress,self).save(*args, **kwargs)
class RelaySisterRelation(models.Model):
id = models.AutoField(primary_key=True,default=0)
relay = models.ForeignKey(RelayAddress,related_name="relay")
sister_relay = models.ForeignKey(RelayAddress,related_name="sister_relay")
My admin.py
class RelaySisterRelationForm(forms.ModelForm):
relay=forms.ModelMultipleChoiceField(label=u'relay',widget=forms.CheckboxSelectMultiple(),queryset=RelayAddress.objects.all())
sister_relay=forms.ModelMultipleChoiceField(label=u'sister_relay',widget=forms.CheckboxSelectMultiple(),queryset=RelayAddress.objects.all())
def save(self, *args, **kwargs):
return super(RelaySisterRelationForm, self).save(*args,**kwargs)
And my view.py is null, then I get a ValueError:
Cannot assign "[<RelayAddress: RelayAddress object>]": "RelaySisterRelation.relay" must be a "RelayAddress" instance.
And how to solve this problem.
RelaySisterRelation.relay is a ForeignKey to RelayAddress meaning it can only store a references to one RelayAddress but your RelaySisterRelationForm.relay uses ModelMultipleChoiceField which is for many-to-many relations so returns a (potentially empty) list of RelayAddress instances.

Django REST Framework different depth for POST/PUT?

I am using Django REST Framework to create an API for my web app. I have a class 'Comment', that has depth=2 set in the Meta class. This works great when GETing the Comments. When I try to send a POST or PUT request though (i.e. create a new Comment) I am told I need to include objects instead of ForeignKey IDs.
Here's my Serializer class:
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
depth = 2
The model:
class Comment(models.Model):
user = models.ForeignKey(User, null=True, blank=True,
related_name='comments')
budget = models.ForeignKey(Budget, related_name='comments')
published = models.BooleanField(default=False)
body = models.TextField()
created = models.DateTimeField(auto_now_add=True)
The view code:
class Comments(generics.ListCreateAPIView):
model = Comment
serializer_class = CommentSerializer
def pre_save(self, obj):
obj.user = self.request.user
And the error that is displayed in the output (JSON) is:
{"user": ["This field is required."], "budget": [{"non_field_errors": ["Invalid data"]}]}
When this raw data is sent:
{"budget": 2, "published": true, "body": "Another comment"}
I know this is a little bit late but I ended up using 2 serializers like so:
class CommentReadSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
depth = 2
class CommentWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
Then used like this:
class Comments(generics.ListCreateAPIView):
model = Comment
serializer_class = CommentReadSerializer
def create(self, request, *args, **kwargs):
serializer = CommentWriteSerializer(data=request.DATA, files=request.FILES)
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data)
serializer = CommentReadSerializer(serializer.object)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
You can set different serializers by overriding the get_serializer_class() function, like so:
def get_serializer_class(self):
method = self.request.method
if method == 'PUT' or method == 'POST':
return YourWriteSerializer
else:
return YourReadSerializer
I thought to add this one, since i came here from Googling after a while.
I believe the proper way to define a serializer field that refers to a foreign key relationship is through something like serializers.PrimaryKeyRelatedField. I don't believe that model serializers automatically use this field class without defining it explicitly in the serializer class.
http://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield
I would imagine that a PrimaryKeyRelatedField serializer would correctly handle JSON data submissions like the one you used in your example.
I had the same problem so I Solved making custom generic methods.This is better implementation of above answers
class CustomListCreateAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
"""
Concrete view for listing a queryset or creating a model instance.
"""
def get_serializer_class(self):
method = self.request.method
if method == 'PUT' or method == 'POST':
return self.writeSerializers
else:
return self.readSerializers
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
Similarily RUD,
class CustomRetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
"""
Concrete view for retrieving, updating or deleting a model instance.
"""
def get_serializer_class(self):
method = self.request.method
if method == 'PUT' or method == 'POST':
return self.writeSerializers
else:
return self.readSerializers
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs) # enter code here
Now I just give writeSerializers and readSerializers values in Views.py
Also to create Read-write Serializers there is an easy way.
class employeeWriteSerializer(serializers.ModelSerializer):
class Meta:
model = employee
fields = ('username','email',..)
class employeeReadSerializer(serializers.ModelSerializer):
class Meta(employeeWriteSerializer.Meta):
depth = 1
It saves time and repetitive work you can also add authentication classes in custom generic Api(Retitve work). Thanks.

Categories

Resources