How to access an object from PK of another object in ModelViewSet - django

The generic structure of the models is that there are teachers and devices, each device has a ForeignKey relationship with the teachers ID/PK.
I'm trying to create my API in such a way that when going to the detail view for a teacher, all of the associated devices are displayed. I've overridden get_serializer_class() to specify which serializer to use at the appropriate time, but can't figure out how to correctly change the Queryset based on detail view or not. Error posted below.
Got AttributeError when attempting to get a value for field `brand` on serializer `DeviceSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Teacher` instance.
Original exception text was: 'Teacher' object has no attribute 'brand'.
class TeacherViewSet(viewsets.ModelViewSet):
queryset = Teacher.objects.order_by('campus','name')
serializer_class = TeacherSerializer
detail_serializer_class = DeviceSerializer
def get_serializer_class(self):
if self.action == 'retrieve':
if hasattr(self, 'detail_serializer_class'):
return self.detail_serializer_class
return super(TeacherViewSet, self).get_serializer_class()
def get_queryset(self, pk=None):
if pk is not None:
return Device.objects.filter(device__owner=self.kwargs.get('pk')
return Teacher.objects.all()

I was able to get the desired output by adding a nested DeviceSerializer in my TeacherSerializer that parses the device object list.
class TeacherSerializer(serializers.ModelSerializer):
devices = DeviceSerializer(many=True)
class Meta:
model = Teacher
fields = ('id', 'name', 'campus', 'email', 'devices')

I assume you are using DRF. If that is the case, just tweak TeacherSerializer to something like:
def TeachSearializer(serializer.ModelSerializer):
devices = serializers.SerializerMethodField()
class Meta:
model = Teacher
fields = '__all__'
def get_devices(self, obj):
return Devices.objects.filter(teacher=obj)
And that is it, everytime you use the serializer on a teacher object, their devices will be added on a field devices

Related

Dynamic nested serializer field Django REST Framework

I am trying to have a serializer of a Parent model, with a nested serializer as a field, being different regarding another string field.
For example, if the registry_type field is equal to intra I want to use _IntraRegistrySerializer to serialize/deserialize the registry_data field, but if registry_type is equal to slack, I want to use _SlackRegistrySerializer for the same field.
I managed to have the serialization working doing this:
class RegistrySerializer(serializers.ModelSerializer):
def to_representation(self, instance):
data = super().to_representation(instance)
if isinstance(instance, IntraRegistry):
data["registry_type"] = "intra"
data["registry_data"] = _IntraRegistrySerializer(instance=instance).data
if isinstance(instance, SlackRegistry):
data["registry_type"] = "slack"
data["registry_data"] = _SlackRegistrySerializer(instance=instance).data
return data
class Meta:
# `Registry` being the parent model of both `IntraRegistry` and `SlackRegistry`
model = Registry
fields = ["id", "description"]
But it only does half of what I would like to do.
I tried overloading methods, using SerializerMethodField, and even though I keep on searching and reading the doc I can't manage to find a solution.
This does seem like a good SerializerMethodField case:
class RegistrySerializer(serializers.ModelSerializer):
registry_type = serializers.SerializerMethodField()
registry_data = serializers.SerializerMethodField()
def get_registry_type(self, obj):
if isinstance(instance, IntraRegistry):
return "intra"
if isinstance(instance, SlackRegistry):
return "slack"
def get_registry_data(self, obj):
if isinstance(instance, IntraRegistry):
return _IntraRegistrySerializer(instance=obj, context=self.context).data
if isinstance(instance, SlackRegistry):
return _SlackRegistrySerializer(instance=obj, context=self.context).data
class Meta:
# `Registry` being the parent model of both `IntraRegistry` and `SlackRegistry`
model = Registry
fields = ["id", "description", "registry_type", "registry_data"]
The above is the implementation of the serializer method fields and should work since both of the registries inherit from Registry.
You may still need to tweak it to your needs. One difference compared to the current implementation is that the registry_type and registry_data will now always be present. They'll be null if this is a Registry object type or some other child type not handled here.

Django Rest Framework model serializer field level validation

I have a DRF ModelSerializer, and I'm trying to override the validation, to no avail.
The reason to override the validation is that the corresponding model field is a postgresql HStoreField, so effectively a python dict. However, the incoming data is an array, and I build the corresponding dict during the create function.
Model Part:
class Report(models.Model):
report = HStoreField()
Serializer:
class ReportSerializer(serializers.ModelSerializer):
class Meta:
model = Report
fields = "__all__"
def create(self, validated_data):
codes = validated_data.pop("report")
report = {code: translate_code(code) for code in codes}
return Report(**validated_data, report=report)
def validate_report(self, value):
print("called")
return type(value) == type([]) # I know this is hacky
So the idea is to translate all of the codes to their respective translations, and save that as a key value pair. This is because I will always need the code and its translation together, and from a performance standpoint it makes more sense to do this once and save it in the db, rather than doing the translation on read.
tl;dr: Model field expects dict, data is actually list, I'm trying to override the validation of this field on the serializer to accept this.
Unfortunately, the validate_report function never seems to be called, and I'm not sure why.
EDIT
I also tried this:
class ReportSerializer(serializers.ModelSerializer):
class Meta:
model = Report
fields = "__all__"
validators = []
def create(self, validated_data):
codes = validated_data.pop("report")
report = {code: translate_code(code) for code in codes}
return Report(**validated_data, report=report)
def validate(self, data):
return isinstance(data["report"], "list")
But this validate() is not called either
EDIT: Viewset:
class ReportsViewset(viewsets.ModelViewSet):
serializer_class = ReportSerializer
viewset = Report.objects.all()

Django REST API Listview

Currently I'm trying to develop personal blog with Django/REST API, and I have trouble for that.
There are a number of posts in blog and I want to control those posts with Hyperlink. I made it by using ModelViewSet, however, whole data in detailView is also shown in ListView.
The thing is, I only want "url" and "title" of posts to be shown in ListView while DetailView contains full data.
Here is my code and current results given by REST framework.
Don't mind IndexView
# serializers
class PostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Post
fields = '__all__'
# views
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = serializers.PostSerializer
permission_classes = (IsAdminUser, )
Post List in REST API:
Post instance in REST API:
As far as I'm aware, you need a separate serializer for the list view.
You could create a custom serializer that takes in a fields arg to select specific fields. But its probably simpler to just have a separate one for the ListView. Also, for the list view, if you are only showing a subset of the model fields, you can use the only() function on the queryset to only return the model data that you need. For example:
qs = MyModel.objects.all().only('field_a', 'field_b', 'field_c')
Here is the custom serializer if you decide to go that way:
class CustomSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
selected_fields = kwargs.pop('selected_fields', None)
# used pop function so selected_fields is not passed to superclass
super().__init__(*args, **kwargs)
if selected_fields:
# make sure only fields for the model are allowed
fields = set(selected_fields)
current_fields = set(self.fields.keys())
for field in current_fields - fields:
self.fields.pop(field)
class MyModelSerializer(CustomSerializer):
class Meta:
model = MyModel
fields = '__all__'
In the list view:
required_fields = ('field_a', 'field_b', 'field_c')
data_to_return = MyModelSerializer(model_queryset, many=True, fields=required_fields).data
return Response(data)

Modify Django Rest Framework ModelViewSet behavior

I basically have the following model in my project:
class ShellMessage(TimeStampedModel):
# There is a hidden created and modified field in this model.
ACTION_TYPE = (
('1' , 'Action 1'),
('2' , 'Action 2')
)
type = models.CharField(max_length=2,choices=ACTION_TYPE,default='1')
action = models.CharField(max_length=100)
result = models.CharField(max_length=300, blank=True)
creator = models.ForeignKey(User)
I created a serializer:
class ShellMessageSerializer(serializers.ModelSerializer):
class Meta:
model = ShellMessage
fields = ('action', 'type', 'result', 'creator')
And a ModelViewSet:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
My issue is the following:
When I create a new ShellMessage with a POST to my API, I don't want to provide the foreignKey of 'creator' but instead just the username of the guy and then process it in my ViewSet to find the user associated with this username and save it in my ShellMessage object.
How can I achieve this using Django rest Framework? I wanted to supercharge create() or pre_save() methods but I'm stuck as all my changes overwrite 'normal' framework behavior and cause unexpected errors.
Thank you.
I finally find my solution just after posting my question :)
So I did the following:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
def pre_save(self, obj):
obj.creator = self.request.user
return super(ShellListViewSet, self).pre_save(obj)
This is working as expected. I hope I did well.
UPDATE: This topic seems to be a duplicate to Editing django-rest-framework serializer object before save
If you intend to intercept and perform some processing before the object gets saved in the model database, then what you're looking for is overriding the method "perform_create" (for POST) or "perform_update" (for PUT/PATCH) which is present within the viewsets.ModelViewSet class.
This reference http://www.cdrf.co/3.1/rest_framework.viewsets/ModelViewSet.html lists all available methods within viewsets.ModelViewSet where you can see that the "create" method calls "perform_create" which in turn performs the actual saving through the serializer object (the object that has access to the model):
def perform_create(self, serializer):
serializer.save()
We can override this functionality that is present in the base class (viewsets.ModelViewSet) through the derived class (the ShellListViewSet in this example) and modify the model attribute(s) that you want to be changed upon saving:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
def findCreator(self):
# You can perform additional processing here to find proper creator
return self.request.user
def perform_create(self, serializer):
# Save with the new value for the target model fields
serializer.save(creator = self.findCreator())
You can also opt to modify the model fields separately and then save (probably not advisable but is possible):
serializer.validated_data['creator'] = self.findCreator()
serializer.save()
Later if the object is already created and you also want to apply the same logic during an update (PUT, PATCH), then within "perform_update" you can either do the same as above through the "serializer.validated_data['creator']" or you could also change it directly through the instance:
serializer.instance.creator = self.findCreator()
serializer.save()
But beware with such updating directly through the instance as from https://www.django-rest-framework.org/api-guide/serializers/ :
class MyModelSerializer(serializers.Serializer):
field_name = serializers.CharField(max_length=200)
def create(self, validated_data):
return MyModel(**validated_data)
def update(self, instance, validated_data):
instance.field_name = validated_data.get('field_name', instance.field_name)
return instance
This means that whatever you assign to the "instance.field_name" object could be overriden if there is a "field_name" data set within the "validated_data" (so in other terms, if the HTTP Body of the PUT/PATCH Request contains that particular "field_name" resulting to it being present in the "validated_data" and thus overriding whatever value you set to the "instance.field_name").

django rest framework change primary key to use a unqiue field

I have a model which is called GameProfile, which is a one to one relation with User model. I used HyperlinkedModelSerializer across all my design.
For the GameProfile, the user field is suppose to be the primary key for querying, it is unique but I did not set it up as a primary key. Is there a way to change the default behavior of django serializer to point to user__id as the primary key and always use it for retreiving the profile in the detail view?
class GameProfileSerializer(serializers.HyperlinkedModelSerializer):
"""
"""
user_pk = serializers.Field(source='user.id')
class Meta:
model = GameProfile
class GameProfileViewSet(viewsets.ModelViewSet):
"""
"""
queryset = GameProfile.objects.all()
serializer_class = GameProfileSerializer
def get_queryset(self):
""" get_queryset
"""
queryset = super(GameProfileViewSet, self).get_queryset()
if not queryset.exists():
raise Http404
if self.request.user.is_authenticated() and not self.request.user.is_superuser:
return queryset.filter(user=self.request.user)
return queryset
please advise, thanks in advance:)
Assuming your GameProfile model looks like:
class GameProfile(models.Model)
user = models.OneToOneField('User')
The serializer will be:
class GameProfileSerializer(serializers.HyperlinkedModelSerializer):
user_id = serializers.Field(source='user.id')
class Meta:
model = GameProfile
Set the .lookup_field attribute on the view correctly:
lookup_field = 'user_id'
Url will be:
/gameprofile/<user_id>
In order to get the URLs to work, you might need to add lookup_field on the ViewSet, not just on the serializer. So you would have:
class GameProfileViewSet(viewsets.ModelViewSet):
queryset = GameProfile.objects.all()
serializer_class = GameProfileSerializer
lookup_field = 'user__id'
In this case the lookup_field uses the double-underscore notation rather than the dot notation (dots won't work in the regular expressions in the URL patterns). I was unable to get the solution proposed by #almalki and #patsweet to work; according to the documentation on serializers, "The value of this option [lookup_field] should correspond both with a kwarg in the URL conf, and with a field on the model", so it's possible that it doesn't work with RelatedFields.
If I'm understanding your question correctly, you want a url structure like so:
/api/<GameProfile-resource>/<user-pk>
If that is the case, you should checkout the lookup_field option. Link
You're Serializer class would look something like:
class GameProfileSerializer(serializers.HyperlinkedModelSerializer):
"""
"""
user_pk = serializers.Field(source='user.id')
class Meta:
model = GameProfile
lookup_field = 'user_pk' # Might have to use 'user__id'