Environment:
Django 2.2.16
Django RESTFramework 3.8.2
Let's say, I wan to group my query_set to a specific format and serializer all models at a time.
In the past, we used a customized model to grouping the type and then feed to the customize serializer.
However, we recently trying to upgrade the Django from 1.11.x to 2.2 and so as DRF.
Then, there's an error prompt and I can't fix it. I also find a link, Look like it's a known issue in DRF.
AttributeError: 'DictField' object has no attribute 'partial'
I defined several models and serializer.
class ModelA(models.Model):
job_id = models.IntegerField()
type = models.CharField(max_length=16)
... Some fields
class ModelASerializer(serializers.ModelSerializer)
class Meta:
model = ModelA
field = '__all__'
class ModelB(models.Model):
Job_id = models.IntegerField()
type = models.CharField(max_length=16)
... Some fields
class ModelBSerializer(serializers.ModelSerializer)
class Meta:
model = ModelB
field = '__all__'
... and many models below
I create a customized model to serialize all models to specific format.
class CustomizeModel:
def __init__(self, Job_id):
self.a = {}
self.b = {}
# In below, I group the query_set by 'type'
a_set = ModelA.objects.filter(job_id=job_id)
for obj in a_set:
if obj.type not in self.a:
self.a[obj.type] = []
self.a[obj.type].append(obj)
b_set = ModelB.objects.filter(job_id=job_id)
for obj in a_set:
if obj.type not in self.b:
self.b[obj.type] = []
self.b[obj.type].append(obj)
class CustomizeSerializer(serializers.Serializer):
a = serializers.DictField(child=ModelASerializer(many=True))
b = serializers.DictField(child=ModelBSerializer(many=True))
My view:
def get_model_data(request, job_id):
...
model = CustomizeModel(job_id)
serializer = CustomizeSerializer(model)
# This serialize.data gives an error
# AttributeError: 'DictField' object has no attribute 'partial'
json_str = json.dumps(serializer.data)
return HttpResponse(json_str)
The code is worked in Django 1.11 and DRF 3.5.4
My expected result is to dump to the JSON format like below
{
a: {type1: [query_set_a filter by job_id and type1],
type2: [query_set_a filter by job_id and type2]},
b: {type1: [query_set_b filter by job_id and type1],
type2: [query_set_b filter by job_id and type2]},
}
However, I tried several ways to rewrite this serializer but got no luck.
class CustomizeModel:
def __init__(self, Job_id):
self.a = ModelA.objects.filter(job_id=job_id)
self.b = ModelB.objects.filter(job_id=job_id)
class CustomizeSerializer(serializers.Serializer):
a = ModelASerializer(many=True)
b = ModelBSerializer(many=True)
It worked. But I don't have a good approach to group_by('type') to my expected format after CustomizeSerializer.
How do I rewrite the DictField in CustomizeSerializer in Django2.2?
Related
I am using Django-import_export for exporting data. I have used the code given below that's not working correctly. It exported only dehydrated data instead of given fields.
class MemberResource(resources.ModelResource):
Brand=Field()
class meta:
model = model
fields=('title','Brand')
def dehydrate_Brand(self, obj):
return str(obj.Brand.title)
class modelAdmin(ImportExportModelAdmin):
resource_class = MemberResource
list_display=['title','Model_code','Chipset','chipset_description','Brand','categories']
search_fields = ['title','Model_code','Chipset',]
fields=('title','Model_code','Chipset','chipset_description','image','Brand','Cat')
admin.site.register(model,modelAdmin)
The name of the Meta subclass is Meta, not meta, so the ModelResource should look like:
class MemberResource(resources.ModelResource):
Brand=Field()
class Meta:
model = Member
fields = ('title','Brand')
def dehydrate_Brand(self, obj):
return str(obj.Brand.title)
I have 3 models: Maker, Item and MakerItem that creates the relation between the items and their makers:
class Maker(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
class Item(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
class MakerItem(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
item_id = models.ForeignKey(Item, on_delete=models.CASCADE)
maker_id = models.ForeignKey(Maker, on_delete=models.CASCADE)
the items can have a random amount of makers.
I want to create both the Item and the MakerItem objects at the same time with a single set of data,
for example if a Maker with id = "abcd" already exists, and I go to /item and send a POST request with the following data:
{
"name": "item1",
"makers": [
{
"maker_id": "abcd"
}
]
}
I want the serializer to create the Item object and the MakerItem object.
I have achieved this, with the following setup:
views.py
class ItemListCreate(ListCreateAPIView):
queryset = Item.objects.all()
serializer_class = ItemSerializer
serializers.py
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True)
class Meta:
model = Item
fields = ['id', 'name', 'makers']
def create(self, validated_data):
maker_item_data = validated_data.pop('makers')
item_instance = Item.objects.create(**validated_data)
for each in maker_item_data:
MakerItem.objects.create(
item_id=check_instance,
maker_id=each['maker_id']
)
return item_instance
but when Django tries to return the created object, it always gives me the error:
AttributeError at /item/
Got AttributeError when attempting to get a value for field `makers` on serializer `ItemSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Item` instance.
Original exception text was: 'Item' object has no attribute 'makers'.
What am I doing wrong?
Thanks
EDIT: To clarify, the objects get created and populate the database correctly, but when the browsable API that DRF provides tries to display the created object, it gives me the error above.
Change:
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True)
To:
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True, source="makeritem_set")
Hope this works!
For clarity, you're attempting to serialise the reverse relationship between MakerItem and Item for this serialiser.
This means that the attribute on your object is automatically set by Django as fieldname_set but you can override this behaviour by setting the related_name kwarg on the field and then makemigrations and migrate it.
In your case you would need to do:
maker_id = models.ForeignKey(Maker, on_delete=models.CASCADE, related_name="maker_items")
And then update the field in the Meta to match the new field name, this way you don't have to manually specify source. Because actually the attribute "makers" is misleading, due to the fact its actually the MakerItem, not the Maker itself.
See https://docs.djangoproject.com/en/3.2/ref/models/relations/ for further details about this behaviour.
Is there a way to group fields in Serializer/ModelSerializer or to modify JSON structure?
There is a Location model:
class Location(Model):
name_en = ...
name_fr = ...
...
If I use ModelSerializer I get plain representation of the object fields like:
{'name_en':'England','name_fr':'Angleterre'}
I want to group some fields under "names" key so I get
{'names':{'name_en':'England','name_fr':'Angleterre'}}
I know I can create custom fields but I want to know if there is a more straightforward way. I tried
Meta.fields = {'names':['name_en','name_fr']...}
which doesn't work
I think it is better using a property. Here is the whole example.
class Location(models.Model):
name_en = models.CharField(max_length=50)
name_fr = models.CharField(max_length=50)
#property
def names(self):
lst = {field.name: getattr(self, field.name)
for field in self.__class__._meta.fields
if field.name.startswith('name_')}
return lst
In views:
class LocationViewSet(viewsets.ModelViewSet):
model = models.Location
serializer_class = serializers.LocationSerializer
queryset = models.Location.objects.all()
And in serializers:
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = ('id', 'names')
My result for my fake data:
[{
"id": 1,
"names": {
"name_en": "England",
"name_fr": "Angleterre"}
}]
Try to create a wrapper serialzer and place the LocationSerializer inside it
class LocationSerialzer(serializers.ModelSerialzer):
name_en = ...
name_fr = ...
...
class MySerializer(serializers.ModelSerializer):
name = LocationSerialzer()
...
Using the above method , you can apply your own customization without being limited to drf custom fields.
You could also not use a property on the model and but use a SerializerMethodField on your serializer like in this implementation.
We used here a _meta.fields, like in the other implementation, to get all the fields that starts with name_ so we can dynamically get the output you desired
class LocationSerializer(serializers.ModelSerializer):
names = serializers.SerializerMethodField()
def get_names(self, obj):
lst = {field.name: getattr(obj, field.name)
for field in obj.__class__._meta.fields
if field.name.startswith('name_')}
return lst
class Meta:
model = Location
fields = ('id', 'names')
Here is my view:
class SectorListAPI(generics.ListAPIView):
queryset = SectorModel.objects.all()
serializer_class = SectorSerializer
Here is my serializers:
class OrganizationSerializer(serializers.ModelSerializer):
class Meta:
model = GroupProfile
fields = ('title','slug',)
class DepartmentSerializer(serializers.ModelSerializer):
organizations = OrganizationSerializer(many=True, read_only=True)
class Meta:
model = DepartmentModel
fields = ('title', 'organizations',)
class SectorSerializer(serializers.ModelSerializer):
# title = serializers.CharField()
departments = DepartmentSerializer(many=True, read_only=True)
class Meta:
model = SectorModel
fields = ('title','departments',)
Look, here 'SectorSerializer' is parent 'DepartmentSerializer' is children and 'OrganizationSerializer' is grand children serializer. Now in my view I can easily filter my queryset for 'SectorModel'. But how can i filter on 'GroupProfile' model.
You might want to filter the queryset to ensure that only results relevant to the currently authenticated user making the request are returned.
You can do so by filtering based on the value of request.user.
For example:
from myapp.models import Purchase
from myapp.serializers import PurchaseSerializer
from rest_framework import generics
class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer
def get_queryset(self):
"""
This view should return a list of all the purchases
for the currently authenticated user.
"""
user = self.request.user
return Purchase.objects.filter(purchaser=user)
EDIT
You can subclass the ListSerializer and overwrite the to_representation method.
By default the to_representation method calls data.all() on the nested queryset. So you effectively need to make data = data.filter(**your_filters) before the method is called. Then you need to add your subclassed ListSerializer as the list_serializer_class on the meta of the nested serializer.
1- subclass ListSerializer, overwriting to_representation and then calling super
2- Add subclassed ListSerializer as the meta list_serializer_class on the nested Serializer.
Code relevant to yours:
class FilteredListSerializer(serializers.ListSerializer):
def to_representation(self, data):
data = data.filter(user=self.request.user, edition__hide=False)
return super(FilteredListSerializer, self).to_representation(data)
class OrganizationSerializer(serializers.ModelSerializer):
class Meta:
list_serializer_class = FilteredListSerializer
model = GroupProfile
fields = ('title','slug',)
class DepartmentSerializer(serializers.ModelSerializer):
organizations = OrganizationSerializer(many=True, read_only=True)
class Meta:
model = DepartmentModel
fields = ('title', 'organizations',)
class SectorSerializer(serializers.ModelSerializer):
# title = serializers.CharField()
departments = DepartmentSerializer(many=True, read_only=True)
class Meta:
model = SectorModel
fields = ('title','departments',)
Thanks to #ans2human for the inspiration behind this answer.
Here's a new approach that is working great for me. I have several Models with is_active = BooleanField(...) that I need to filter out in nested relationships.
NOTE: this solution does not filter out results on non-list fields. for that, you should look to the primary queryset on your View
The core of the work is done by overloading the to_representation() function on a custom ListSerializer, and the many_init on an accompanying custom ModelSerializer:
class FilteredListSerializer(serializers.ListSerializer):
filter_params:dict
def __init__(self, *args, filter_params:dict={"is_active":True}, **kwargs):
super().__init__(*args, **kwargs)
self.filter_params = filter_params
def set_filter(self, **kwargs):
self.filter_params = kwargs
def to_representation(self, data):
data = data.filter(**self.filter_params)
return super().to_representation(data)
class FilteredModelSerializer(serializers.ModelSerializer):
LIST_SERIALIZER_KWARGS = serializers.LIST_SERIALIZER_KWARGS + ("filter_params",)
LIST_ONLY_KWARGS = ('allow_empty', 'filter_params')
#classmethod
def many_init(cls, *args, **kwargs):
list_kwargs = dict()
for arg in cls.LIST_ONLY_KWARGS:
value = kwargs.pop(arg, None)
if value is not None:
list_kwargs[arg] = value
child_serializer = cls(*args, **kwargs, **({"read_only":True} if "read_only" not in kwargs else dict()))
list_kwargs['child'] = child_serializer
list_kwargs.update({
key: value for key, value in kwargs.items()
if key in cls.LIST_SERIALIZER_KWARGS
})
meta = getattr(cls, 'Meta', None)
list_serializer_class = getattr(meta, 'list_serializer_class', FilteredListSerializer)
return list_serializer_class(*args, **list_kwargs)
Then, your custom ModelSerializer for whatever view would instead just extend FilteredModelSerializer instead.
class ChildSerializer(FilteredModelSerializer):
is_active = BooleanField() # not strictly necessary, just for visibilty
... # the rest of your serializer
class ParentSerializer(serializers.ModelSerializer):
children = ChildSerializer(many=True)
...# the rest of your parent serializer
Now, the children field on the ParentSerializer will filter for is_active = True.
If you have a custom query that you wanted to apply, you can do so by providing a dict of filter params in the standard queryset format:
class ParentSerializer(serializers.ModelSerializer):
children = ChildSerializer(many=True, filter_params={"my_field":my_value, "my_datetime__gte": timezone.now()})
...# the rest of your parent serializer
Alternatively, one could also utilize the set_filter(...) method on the FilteredListSerializer after instantiating the field, like so. This will yield a more familiar format to the original QuerySet.filter(...) style:
class ParentSerializer(serializers.ModelSerializer):
children = ChildSerializer(many=True)
children.set_filter(my_field=my_value, my_datetime__gte=timezone.now())
...# the rest of your parent serializer
I have this code
# Models
class NestedSample(models.Model):
something = models.CharField(max_length=255)
class Sample(models.Model):
thing = models.BooleanField()
nested = models.ForeignKey(NestedSample)
# Serializers
class NestedSampleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = api_models.NestedSample
class SampleSerializer(serializers.HyperlinkedModelSerializer):
nested = NestedSampleSerializer() # HERE filter delete=false
nested2 = NestedSample2Serializer() # HERE filter deletefalse
class Meta:
model = api_models.Sample
In my view I am overrding the queryset for delete=False but it is not applying to nested serializers.
delete=False in queryset will only filter Sample. To filter queryset in nested serializer you can use serializers.ListSerializer like:
class FilterDeleteListSerializer(serializers.ListSerializer):
def to_representation(self, data):
data = data.filter(delete=False)
return super(FilterDeleteListSerializer, self).to_representation(data)
class NestedSampleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = api_models.NestedSample
list_serializer_class = FilterDeleteListSerializer
class NestedSample2Serializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = api_models.NestedSample2
list_serializer_class = FilterDeleteListSerializer
class SampleSerializer(serializers.HyperlinkedModelSerializer):
nested = NestedSampleSerializer() # HERE filter delete=false
nested2 = NestedSample2Serializer() # HERE filter deletefalse
class Meta:
model = api_models.Sample
Learn more here
I didn't exactly understand your question, but from what I figured you've got a boolean field in your Model which is set to True if you delete the object instead of actually deleting it from the database (SQL DELETE).
Now coming to your question, if you just want to filter the nested serializer then you could use the SerializerMethodField. You need to specify the method to call as an argument or add a method with the name 'get_' followed by the field name. In this method you can filter the queryset serialize it and return the data of that queryset.
class UserSerializer(serializers.ModelSerializer):
delete_filtered_items = serializers.SerializerMethodField()
class Meta:
model = User
def get_delete_filtered_items(self, obj):
items = Item.objects.filter(user=obj,deleted=False)
serializer = ItemsSerializer(instance=items, many=True)
return serializer.data
The above solution should work for your requirements, but if what you've implemented is similar to a soft delete then it would seem cleaner and more moduler to create a custom model manager.