Copying only non-relation attributes django/python - python

I am copying a model object to another, but I want that it doesn’t copy the relations
For example, assume you have a model like this:
class Dish(models.Model):
name = models.CharField(max_length=100)
description = models.CharField(max_length=500)
category = models.ForeignKey(Category, on_delete=models.CASCADE, default=1)
def __str__(self):
return self.name
Then I do:
my_dish = Dish.objects.get(pk=dish.id)
serializer = Dish_Serializer(my_dish)
my_new_object = serializer.data
I want my_new_object to include only those attributes that are not relations, in this case, name and description.
How do I do that without accessing name and description directly?

I assume in your serializer you don't want to explicitly define which field to serialize. Otherwise you could do the following:
class Dish_Serializer(serializers.ModelSerializer):
class Meta:
model = Dish
fields = ['id','name', 'description']
You probably can define these fields dynamically:
fields = [f.name for f in Dish._meta.concrete_fields]
or
fields = [f.name for f in Dish._meta.fields if not isinstance(f,ForeignKey)]

Ultimately, you want my_new_object in dictionary format and as per condition pk will give you only one object of dish.
So, you can do this instead :
my_new_object = Dish.objects.filter(pk=dish.id).values("name", "description")[0]
It will give you exact what you want, just declare the fields you need in values as an attribute fields.

You can remove a field from your serializer using .fields.pop(field_name) method like the below example According that I took from Dynamically modifying fields [drf-docs]:
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super().__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` >argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
Also, you can do this in your view like the below code snippet:
my_dish = Dish.objects.get(pk=dish.id)
serializer = Dish_Serializer(my_dish)
desired_fields = {'id', 'name', 'description'}
all_fields = set(serializer.fields)
for field in all_fields:
if field not in desired_fields:
serializer.fields.pop(field)
my_new_object = serializer.data

Related

How to filter a nested serializer's field in Django DRF

I have two models named 'School' and 'Student'. I've created each's serializers and the nested serializer for School having a student serializer as a nested field.
Here I want to apply filters on the fields of the serializers using 'django-filters' and it is almost working, BUT ...the problem is that when I filter the nested field, i-e 'students's field' , It doesn't show me the required result.
My models are :
class School(models.Model):
name = models.CharField(max_length=256)
principal = models.CharField(max_length=256)
location = models.CharField(max_length=256)
is_government = models.BooleanField(default=True)
def __str__(self):
return self.name
class Student(models.Model):
name = models.CharField(max_length=256)
age = models.PositiveIntegerField()
school = models.ForeignKey(School,related_name='students',on_delete = models.CASCADE)
is_adult = models.BooleanField(default=True)
def __str__(self):
return self.name
and my serializers are:
class SchoolSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
# Instantiate the superclass normally
super(SchoolSerializer, self).__init__(*args, **kwargs)
allow_students = self.context.get("allow_students",None)
if allow_students:
self.fields['students'] = StudentSerializer(many=True, context=kwargs['context'], fields=['name','age','is_adult'])
class Meta():
model = School
fields = '__all__'
class StudentSerializer(DynamicFieldsModelSerializer):
class Meta():
model = Student
fields = '__all__'
and these are the filters that i am using in my views:
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import FilterSet
from django_filters import rest_framework as filters
class SchoolStudentAPIView(generics.ListAPIView, mixins.CreateModelMixin):
queryset = School.objects.all()
serializer_class = SchoolSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields = ('is_government','students__is_adult')
Here, the issue is that when i search for "students__is_adult", which is a nested field, It filters out the list of students that are adult ALONG WITH THE students that are not.
Can someone add something extra or give another solutuion? thank you
The problem
First of all, Django Rest Framework is not doing the query you'd expect. Let's see how to check.
One way to debug the actual query is adding a custom list() method to the SchoolStudentAPIView class, as follows:
def list(self, request, *args, **kwargs):
resp = super().list(request, *args, **kwargs)
from django.db import connection
print(connection.queries) # or set a breakpoint here
return resp
This method does nothing more than dumping all the executed queries to the console.
The last element of connection.queries is what we should focus on. It'll be a dict() with its "sql" key looking something like:
SELECT `school`.`id`, `school`.`name`, `school`.`location`, `school`.`is_government`
FROM `school` INNER JOIN `student` ON (`school`.`id` = `student`.`school_id`)
WHERE `student`.`is_adult` = 1
This query means that the SchoolSerializer will be passed all the Schools that have at least one adult Student.
By the way, the same school can appear multiple times, since the above query produces one row per adult student.
The SchoolSerializer, in the end, shows all the Students in the School regardless of any filtering option: this is what this line achieves.
if allow_students:
self.fields['students'] = StudentSerializer(many=True, ...)
Suggested solution
No simple solution is to be found with serializers. Maybe the more straightforward way is to write a custom list() method in the SchoolStudentAPIView class.
The method will:
look for the query string argument student__is_adult: if it's there, the method will create a custom field on each School in the queryset (I named it filtered_students), and make that field point to the correct Student queryset.
pass a context argument to the SchoolSerializer, to tell it that students are filtered
The SchoolSerializer class, in turn, will populate its students field in two different ways, depending on the presence or absence of the context argument. Specifically, the StudentSerializer field will have the source kwarg if the students__is_adult key is present in the passed context.
In code:
class SchoolStudentAPIView(generics.ListAPIView, mixins.CreateModelMixin):
# ...
def list(self, request, *args, **kwargs):
schools = self.get_queryset()
ctx = {}
if 'students__is_adult' in request.query_params:
filter_by_adult = bool(request.query_params['students__is_adult'])
ctx = {
'students__is_adult': filter_by_adult,
'allow_students': True,
}
for s in schools:
s.filtered_students = s.students.filter(is_adult=filter_by_adult)
ser = SchoolSerializer(data=schools, many=True, context=ctx)
ser.is_valid()
return Response(ser.data)
class SchoolSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super(SchoolSerializer, self).__init__(*args, **kwargs)
allow_students = self.context.get("allow_students", None)
if allow_students:
# Change 'source' to custom field if students are filtered
filter_active = self.context.get("posts__is_active", None)
if filter_active is not None:
stud = StudentSerializer(
source='filtered_students', many=True,
context=kwargs['context'],
fields=['name', 'age', 'is_adult'])
else:
stud = StudentSerializer(
many=True, context=kwargs['context'],
fields=['name', 'age', 'is_adult'])
self.fields['students'] = stud

How to make a field editable=False in DRF

I've a serializer. I want to restrict updating a field. How would I do that?
class ABCSerializer(serializers.ModelSerializer):
class Meta:
"""Meta."""
model = ModelA
fields = ('colA', 'colB', 'colC',)
colA is a required field while creating the object. However, it should not be allowed to update. How can I do that??
Sounds like you need different serializers for PUT and POST methods. In the serializer for the PUT method you can set the colA field to readonly
class ABCViewSet(ModelViewSet):
serializer_class = ABCSerializer
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method == 'PUT':
serializer_class = SerializerWithReadOnlyColA
return serializer_class
You can use Django REST Frameworks field-level validation by validating that field has not changed on update like so:
from rest_framework.exceptions import ValidationError
class ABCSerializer(serializers.ModelSerializer):
colA = serializers.CharField(max_length=100)
def validate_colA(self, value):
if self.instance and self.instance.colA != value:
raise ValidationError("You may not edit colA")
return value
class Meta:
"""Meta."""
model = ModelA
fields = ('colA', 'colB', 'colC',)
This will check whether or not this is an update (via checking if an instance is populated on the serializer) and if so it will then check to see if you have made a change to the field and if you have it will throw a ValidationError. The benefit of this approach is that you can keep your view code the same as before and continue to keep your validation behaviour in your serializer.
You can override the serializer's update method to only update fields that you want.
class ABCSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
instance.colB = validated_data.get('colB', instance.colB)
instance.colC = validated_data.get('colC', instance.colC)
# do nothing to instance.colA
instance.save()
return instance
class Meta:
model = ModelA
fields = ('colA', 'colB', 'colC',)
Or if you have many fields, and just want to omit updating colA, you could write your update method like this:
def update(self, instance, validated_data):
validated_data.pop('colA') # validated_data no longer has colA
return super().update(instance, validated_data)
You can read more about overriding update here: https://www.django-rest-framework.org/api-guide/serializers/#saving-instances
I think it's too late to answer but this may be useful for others:)
you can solve your problem this way:
class ABCSerializer(serializers.ModelSerializer):
class Meta:
model = ModelA
fields = ('colA', 'colB', 'colC',)
def get_fields(self):
fields = super().get_fields()
if self.instance:
fields["colA"].read_only = True
return fields
When you want to create, the self.instance is None, it will pass the if clause, and in case of updating the if clause will make the field read only and non-editable.
You can do this with the read_only_fieldsoption
class ABCSerializer(serializers.ModelSerializer):
class Meta:
"""Meta."""
model = ModelA
fields = ('colB', 'colC',)
read_only_fields = ('colA',)

SlugRelatedField queryset

I am struggling to figure out the queryset for SlugRelatedField.
My data is such that I have a bunch of Object instances that belong to a Project. A project has a unique 'top' Object. Objects can have the same name only if they below to different Projects.
class Object(models.Model):
project = models.ForeignKey('Project', null=False, related_name='objs')
name = models.TextField(null=False, db_index=True)
....
class Meta:
index_together = unique_together = ('project', 'name')
class Project(models.Model):
user = models.ForeignKey(get_user_model(), null=False, related_name='+')
name = models.TextField(null=False)
top = models.OneToOneField(Object, null=True, related_name='+')
....
class ObjectSerializer(NonNullSerializer):
class Meta:
model = Object
fields = ('name',)
class ProjectSerializer(NonNullSerializer):
objs = ObjectSerializer(many=True, required=False)
top = serializers.SlugRelatedField(slug_field='name', queryset=Object.objects.filter(????))
class Meta:
model = Project
fields = ('id', 'name', 'objs', 'top')
What is my queryset going to look like for top if I want to find only only the one Object that belongs to the correct Project? In other words, how to deserialize this:
[{
'name' : 'Project1',
'objs' : [{
'name': 'One'
}],
'top': 'One'
},
{
'name' : 'Project2',
'objs' : [{
'name': 'One'
}],
'top': 'One' <-- This should point to One under Project2, not One under Project1
}]
I was just revisiting my own question on this topic when I was lead back to here, so here's a way of achieving this (I think).
class ObjectSerializer(NonNullSerializer):
class Meta:
model = Object
fields = ('name',)
class TopSerializerField(SlugRelatedField):
def get_queryset(self):
queryset = self.queryset
if hasattr(self.root, 'project_id'):
queryset = queryset.filter(project_id=project_id)
return queryset
class ProjectSerializer(NonNullSerializer):
def __init__(self, *args, **kwargs):
self.project_id = kwargs.pop('project_id')
super().__init__(*args, **kwargs)
# I've needed this workaround for some cases...
# def __new__(cls, *args, **kwargs):
# """When `many=True` is provided then we need to attach the project_id attribute to the ListSerializer instance"""
# project_id = kwargs.get('project_id')
# serializer = super(ProjectSerializer, cls).__new__(cls, *args, **kwargs)
# setattr(serializer, 'project_id', project_id)
# return serializer
objs = ObjectSerializer(many=True, required=False)
top = TopSerializerField(slug_field='name', queryset=Object.objects.all())
class Meta:
model = Project
fields = ('id', 'name', 'objs', 'top')
When you go to deserialize the data, it would search for objects that belong to the correct project defined on the serializer.
I have a solution that solves this problem in my case, which I will try to explain here.
The problem, abstracted:
Suppose I have a hierarchy with Foo as the top-level objects, each associated with several Bars:
class Foo(Model):
pass
class Bar(Model):
bar_text = CharField()
foo = ForeignKey(Foo, related_name='bars')
Then I can use SlugRelatedField trivially for read only serializations of Foo, by which I mean the serializer:
class FooSerializer(ModelSerializer):
bars = serializers.SlugRelatedField(slug_field='bar_text',
many=True, read_only=True)
class Meta:
model = Foo
fields = ('bars',)
will produce serializations like:
{ 'bars' : [<bar_text>, <bar_text>, ...] }
However, this is read only. To allow writing, I have to provide a queryset class attribute outside of any methods. The problem is, because we have a Foo->Bar hierarchy, we don't know what the queryset is outside of any request. We would like to be able to override a get_queryset() method, but none seems to exist. So we can't use SlugRelatedField. What horribly hacky way can we fix it?
My Solution:
First, add an #property to the Foo model and put this property in the serializer:
In models.py:
class Foo(Model):
#property
def bar_texts(self):
return [bar.bar_text for bar in self.bars.all()]
In serializers.py:
class FooSerializer(ModelSerializer):
class Meta:
model = Foo
fields = ('bar_texts',)
This allows for the bar texts to be serialized as before, but we still can't write (we can try - the framework won't reject it but it will hit an exception when trying to save the bar_texts attribute of a Foo)
So, the hacky part - fix perform_create() in the Foo list view.
class FooList:
def perform_create(self, serializer):
# The serializer contains the bar_text field, which we want, but doesn't correspond
# to a writeable attribute of Foo. Extract the strings and save the Foo. Use pop with a default arg in case bar_texts isn't in the serialized data
bar_texts = serializer.validated_data.pop('bar_texts', [])
# Save the Foo object; it currently has no Bars associated with it
foo = serializer.save()
# Now add the Bars to the database
for bar_text in bar_texts:
foo.bars.create(bar_text=bar_text)
I hope that makes sense. It certainly works for me, but I have get to find any glaring bugs with it

django rest framework and model inheritance

I have an "abstract" model class MyField:
class MyField(models.Model):
name = models.CharField(db_index = True, max_length=100)
user = models.ForeignKey("AppUser", null=False)
I have a few other subclasses of MyField each defining a value of a specific type.
for example:
class MyBooleanField(MyField):
value = models.BooleanField(db_index = True, default=False)
In MyField I have a method get_value() that returns the value based on the specific subclass.
In django rest I want to fetch all the fields of a user
class AppUserSerializer(serializers.ModelSerializer):
appuserfield_set = MyFieldSerializer(many=True)
class Meta:
model = AppUser
fields = ('appuser_id', 'appuserfield_set')
On the client side I want the user to be able to add new fields and set values to them and then on the server I want to be able to create the correct field based on the value.
What is the correct way to achieve this behavior?
After some digging, here is what I ended up doing. Aside from the code below I had to implement get_or_create and create the relevant subclass of MyField based on the passed value.
class ValueField(serializers.WritableField):
#called when serializing a field to a string. (for example when calling seralizer.data)
def to_native(self, obj):
return obj;
"""
Called when deserializing a field from a string
(for example when calling is_valid which calles restore_object)
"""
def from_native(self, data):
return data
class MyFieldSerializer(serializers.ModelSerializer):
value = ValueField(source='get_value', required=False)
def restore_object(self, attrs, instance=None):
"""
Called by is_valid (before calling save)
Create or update a new instance, given a dictionary
of deserialized field values.
Note that if we don't define this method, then deserializing
data will simply return a dictionary of items.
"""
if instance:
# Update existing instance
instance.user = attrs.get('user', instance.user)
instance.name = attrs.get('name', instance.name)
else:
# Create new instance
instance = MyField.get_or_create(end_user=attrs['user'],
name=attrs['name'],
value=attrs['get_value'])[0]
instance.value = attrs['get_value']
return instance
def save_object(self, obj, **kwargs):
#called when saving the instance to the DB
instance = MyField.get_or_create(end_user=obj.user,
name=obj.name,
value=obj.value)[0]
class Meta:
model = MyField
fields = ('id', 'user', 'name', 'value')

Store form fields as key-values / individual rows

I have a simple form in Django that looks like this:
class SettingForm(forms.Form):
theme = forms.CharField(rrequired=True,
initial='multgi'
)
defaultinputmessage = forms.CharField(required=True,
initial='Type here to begin..'
)
...and the model to store it looks like:
class Setting(models.Model):
name = models.CharField(
null=False, max_length=255
)
value= models.CharField(
null=False, max_length=255
)
When the form is submitted, how can i store the form fields as key value pairs and then when the page is rendered, how can I initialize the form with the key's value. I've tried looking for an implementation of this but have been unable to find one.
Any help?
Thanks.
I'm assuming you want to store 'theme' as the name and the value as the value, same for defaultinputmessage. If that's the case, this should work:
form = SettingForm({'theme': 'sometheme', 'defaultinputmessage': 'hello'})
if form.is_valid():
for key in form.fields.keys():
setting = Setting.objects.create(name=key, value=form.cleaned_data[key])
Here's how I did it.
I needed to do this because I had a Model that stored information as key value pairs and I needed to build a ModelForm on that Model but the ModelForm should display the key-value pairs as fields i.e. pivot the rows to columns. By default, the get() method of the Model always returns a Model instance of itself and I needed to use a custom Model. Here's what my key-value pair model looked like:
class Setting(models.Model):
domain = models.ForeignKey(Domain)
name = models.CharField(null=False, max_length=255)
value = models.CharField(null=False, max_length=255)
objects = SettingManager()
I built a custom manager on this to override the get() method:
class SettingManager(models.Manager):
def get(self, *args, **kwargs):
from modules.customer.proxies import *
from modules.customer.models import *
object = type('DomainSettings', (SettingProxy,), {'__module__' : 'modules.customer'})()
for pair in self.filter(*args, **kwargs): setattr(object, pair.name, pair.value)
setattr(object, 'domain', Domain.objects.get(id=int(kwargs['domain__exact'])))
return object
This Manager would instantiate an instance of this abstract model. (Abstract models don't have tables so Django doesn't throw up errors)
class SettingProxy(models.Model):
domain = models.ForeignKey(Domain, null=False, verbose_name="Domain")
theme = models.CharField(null=False, default='mytheme', max_length=16)
message = models.CharField(null=False, default='Waddup', max_length=64)
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super(SettingProxy, self).__init__(*args, **kwargs)
for field in self._meta.fields:
if isinstance(field, models.AutoField):
del field
def save(self, *args, **kwargs):
with transaction.commit_on_success():
Setting.objects.filter(domain=self.domain).delete()
for field in self._meta.fields:
if isinstance(field, models.ForeignKey) or isinstance(field, models.AutoField):
continue
else:
print field.name + ': ' + field.value_to_string(self)
Setting.objects.create(domain=self.domain,
name=field.name, value=field.value_to_string(self)
)
This proxy has all the fields that I'd like display in my ModelFom and store as key-value pairs in my model. Now if I ever needed to add more fields, I could simply modify this abstract model and not have to edit the actual model itself. Now that I have a model, I can simply build a ModelForm on it like so:
class SettingsForm(forms.ModelForm):
class Meta:
model = SettingProxy
exclude = ('domain',)
def save(self, domain, *args, **kwargs):
print self.cleaned_data
commit = kwargs.get('commit', True)
kwargs['commit'] = False
setting = super(SettingsForm, self).save(*args, **kwargs)
setting.domain = domain
if commit:
setting.save()
return setting
I hope this helps. It required a lot of digging through the API docs to figure this out.

Categories

Resources