I am trying to create a rather simple model, all the model holds is a week number (as the primary key), and a oneToMany field with a list of users. The idea is that it should function like a schema, where you can see which users is attached to a specific week number. My problem is currently getting the serializer to work with the oneToMany field.
Model:
class Schema(models.Model):
week = models.PositiveIntegerField(primary_key=True,
unique=True,
validators=[MinValueValidator(1), MaxValueValidator(53)],
)
users = models.ForeignKey(MyUser, null=True, on_delete=models.SET_NULL)
class Meta:
ordering = ('week',)
Serializer:
class SchemaSerializer(serializers.Serializer):
class Meta:
model = Schema
fields = ('week', 'users')
def create(self, validated_data):
answer, created = Schema.objects.update_or_create(
week=validated_data.get('week', 1),
defaults={'users', validated_data.get('users', None)}
)
return answer
View:
class SchemaView(APIView):
permission_classes = (IsAuthenticated, IsAdminUser)
def get(self, request):
schemas = Schema.objects.all()
serializer = SchemaSerializer(schemas)
return Response(serializer.data)
def post(self, request):
data = request.data
serializer = SchemaSerializer(data=data)
serializer.is_valid()
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
I get the following error TypeError: cannot convert dictionary update sequence element #0 to a sequence. As I interpret that error, something is wrong with the first element (week number) when trying to do serializer.save().
I have created a custom field in the client serializer.
The value of this field is calculated by a complex serializer method.
class ClientsStatsSerializer(serializers.ModelSerializer):
"""
Serializer shows total_spend for 2019 by client.
"""
refs_count = serializers.SerializerMethodField()
total_spend_2019 = serializers.SerializerMethodField()
class Meta:
model = Company
ordering = ('total_spend_2019',)
fields = [
'id',
'legal_name',
'refs_count',
'total_spend_2019',
]
def get_total_spend_2019(self, obj):
...
I would like to get the output sorted by the value of total_spend_2019. It looks like I cannot do it here with a simple ordering = ('total_spend_2019',) I cannot do it either in the model, neither in the view.
EDIT: It would be great to have a generic solution that would work with any SerializerMethodField.
Current view is as such:
class ClientsStatsViewSet(viewsets.ViewSet):
def list(self, request):
queryset = request.user.company.clients.all()
client_id = self.request.query_params.get('client_id', None)
if client_id is not None:
queryset = queryset.filter(pk=client_id)
serializer = ClientsStatsSerializer(queryset, many=True)
return Response(serializer.data)
Any idea how to solve this?
Thank you very much!!
All right, I managed to solve it with the following:
class ClientsStatsViewSet(viewsets.ViewSet):
def list(self, request):
queryset = request.user.company.clients.all()
client_id = self.request.query_params.get('client_id', None)
if client_id is not None:
queryset = queryset.filter(pk=client_id)
serializer = ClientsStatsSerializer(queryset, many=True)
serializer_data = sorted(
serializer.data, key=lambda k: k['total_spend_2019'], reverse=True)
return Response(serializer_data)
I don't know if it is the most performant way to do this, but it works.
Also you can try create your custom model manager. This is popular decision for ordering data:
models.py
class CustomManager(models.Manager):
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(...).order_by(...)
class Company(models.Model):
...
objects = models.Manager
custom_manager = CustomManager()
views.py
class CompanyViewSet(viewsets.ViewSet):
...
queryset = Company.custom_manager.all()
...
It's possible to change a serializer depth with a GET parameter? For example calling http://localhost:8000/api-auth/?depth=1
The proper way to pass some extra data from view to serializer is the serializer context.
In DRF class-based view, you can (and, actually, should for such purposes) override get_serializer_context() method. In the overridden method you just add to the context, which is just a dictionary, whatever you want.
Simple example, how to do this in view:
class YourView(generics.RetrieveAPIView):
def get_serializer_context(self):
context = super().get_serializer_context()
context['depth'] = self.request.query_params.get('depth', 1)
return context
And the to access it in serializer:
class YourSerializer(serializers.ModelSerializer):
the_depth_from_get_param = serializers.SerializerMethodField()
class Meta:
model = YourModel
fields = [
'the_depth_from_get_param'
]
def get_the_depth_from_get_param(self, obj):
return self.context['depth']
I've solved it in a really simple way, assuming that views are made with class based views or viewsets:
The serializer_class propriety it's actually the serializer, it's a class, so you can access to the depth value with serializer_class.Meta.depth, and assign it a value from the self.request.GET array
Example serializer:
class ItemSerializer(ModelSerializer):
class Meta:
model = Item
fields = '__all__'
Example view:
class ItemList(generics.ListCreateAPIView):
queryset = Item.objects.all()
serializer_class = serializers.ItemSerializer
def get_serializer_class(self):
if(self.request.GET.get('depth')):
if (10 >= int(self.request.GET.get('depth')) >= 0):
self.serializer_class.Meta.depth = int(
self.request.GET.get('depth'))
return self.serializer_class
I am currently using restful and serializers to create and update my user.
Somehow I am not able to update some of the fields if the field has to do with OneToOneField / ForeignKey.
in my models.py, my Student is actually connected to the django build in user model which includes the user's email and connected to the school model which has the name of the school
class Student(Model):
user = OneToOneField(settings.AUTH_USER_MODEL, on_delete=CASCADE)
date_of_birth = DateField(blank=True, null=True)
student_name = CharField(max_length=256)
school = ForeignKey(School,
on_delete=CASCADE,
related_name="%(class)ss",
related_query_name="%(class)s",
blank=True,
null=True)
in serializer.py I have
class StudentSerializer(ModelSerializer):
user_email = SerializerMethodField()
school_name = SerializerMethodField()
class Meta:
model = Student
fields = (
'user_email', 'student_name', 'phone', 'school_name')
def get_user_email(self, obj):
return obj.user.email
def get_school_name(self, obj):
return obj.school.school_name
def create(self, validated_data):
return Student.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.user.email = validated_data.get('user_email', instance.user.email)
instance.student_name = validated_data.get('student_name', instance.student_name)
instance.phone = validated_data.get('phone', instance.phone)
instance.school.school_name = validated_data.get('school_name', instance.school.school_name)
instance.save()
return instance
in my view.py update function
class UserViewSet(ViewSet):
queryset = Student.objects.all()
def update(self, request, pk=None):
student = get_object_or_404(self.queryset, pk=pk)
serializer = StudentSerializer(student, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response({'status': True})
return Response({'status': False, 'message': serializer.errors})
I am able to use the API view to pass in json and update the student_name and phone but as for the other two, user_email and school_name I am not able to update it. I don't get any error output when I submit the json though.
I realized the two fields that I am not able to update are because they OneToOneField / ForeignKey.
Can someone please give me a hand what I am missing here or what I can do to check?
Thanks in advance
I think your serializer isn't completed... the field of user and school is instance model, you need specific field in your serializer to implement the instance model, eg: with source='...' argument.
and example:
class VoteSerializer(serializers.ModelSerializer):
# by `username`
user = serializers.CharField(
source='user.username',
read_only=True
)
# by `pk/id`
candidate = serializers.IntegerField(
source='candidate.pk',
read_only=True
)
class Meta:
model = Vote
fields = ('user', 'candidate', 'score')
def create(self, validated_data):
return Vote.objects.create(**validated_data)
and in your case, perhaps is like this;
class StudentSerializer(ModelSerializer):
# by `pk/id` from the user
user = serializers.IntegerField(
source='user.pk',
read_only=True
)
school = serializers.IntegerField(
source='school.pk',
read_only=True
)
Since you are using SerializerMethodField which is readonly field (docs) for user_email and school_name so they won't be available in the validated_data.
Have you check the data you are receiving in validated_data
def update(self, instance, validated_data):
print('++'*22, validated_data)
return instance
The nested seriailzer / model / presentation actually helped me get the work done and pretty helpful.
An example is also provided here.
http://www.django-rest-framework.org/api-guide/serializers/#writing-update-methods-for-nested-representations
the above is continued from
http://www.django-rest-framework.org/api-guide/serializers/#writing-create-methods-for-nested-representations which contained how the nested serializer is being setup in the class and meta's fields
I want to hide specific fields of a model on the list display at persons/ and show all the fields on the detail display persons/jane
I am relatively new to the rest framework and the documentation feels like so hard to grasp.
Here's what I am trying to accomplish.
I have a simple Person model,
# model
class Person(models.Model):
first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=30, blank=True)
nickname = models.CharField(max_length=20)
slug = models.SlugField()
address = models.TextField(max_length=300, blank=True)
and the serializer class
# serializers
class PersonListSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ('nickname', 'slug')
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')
and the viewsets.
# view sets (api.py)
class PersonListViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonListSerializer
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonSerializer
at the url persons I want to dispaly list of persons, just with fields nickname and slug and at the url persons/[slug] I want to display all the fields of the model.
my router configurations,
router = routers.DefaultRouter()
router.register(r'persons', api.PersonListViewSet)
router.register(r'persons/{slug}', api.PersonViewSet)
I guess the second configuration is wrong, How can I achieve what I am trying to do?
update:
the output to persons/slug is {"detail":"Not found."} but it works for person/pk
Thank you
For anyone else stumbling across this, I found overriding get_serializer_class on the viewset and defining a serializer per action was the DRY-est option (keeping a single viewset but allowing for dynamic serializer choice):
class MyViewset(viewsets.ModelViewSet):
serializer_class = serializers.ListSerializer
permission_classes = [permissions.IsAdminUser]
renderer_classes = (renderers.AdminRenderer,)
queryset = models.MyModel.objects.all().order_by('-updated')
def __init__(self, *args, **kwargs):
super(MyViewset, self).__init__(*args, **kwargs)
self.serializer_action_classes = {
'list':serializers.AdminListSerializer,
'create':serializers.AdminCreateSerializer,
'retrieve':serializers.AdminRetrieveSerializer,
'update':serializers.AdminUpdateSerializer,
'partial_update':serializers.AdminUpdateSerializer,
'destroy':serializers.AdminRetrieveSerializer,
}
def get_serializer_class(self, *args, **kwargs):
"""Instantiate the list of serializers per action from class attribute (must be defined)."""
kwargs['partial'] = True
try:
return self.serializer_action_classes[self.action]
except (KeyError, AttributeError):
return super(MyViewset, self).get_serializer_class()
Hope this helps someone else.
You can override the 'get_fields' method your serializer class and to add something like that:
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
request = self.context.get('request')
if request is not None and not request.parser_context.get('kwargs'):
fields.pop('your_field', None)
return fields
In this case when you get detail-view there is 'kwargs': {'pk': 404} and when you get list-view there is 'kwargs': {}
I wrote an extension called drf-action-serializer (pypi) that adds a serializer called ModelActionSerializer that allows you to define fields/exclude/extra_kwargs on a per-action basis (while still having the normal fields/exclude/extra_kwargs to fall back on).
The implementation is nice because you don't have to override your ViewSet get_serializer method because you're only using a single serializer. The relevant change is that in the get_fields and get_extra_kwargs methods of the serializer, it inspects the view action and if that action is present in the Meta.action_fields dictionary, then it uses that configuration rather than the Meta.fields property.
In your example, you would do this:
from action_serializer import ModelActionSerializer
class PersonSerializer(ModelActionSerializer):
class Meta:
model = Person
fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')
action_fields = {
'list': {'fields': ('nickname', 'slug')}
}
Your ViewSet would look something like:
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonSerializer
And your router would look normal, too:
router = routers.DefaultRouter()
router.register(r'persons', api.PersonViewSet)
Implementation
If you're curious how I implemented this:
I added a helper method called get_action_config which gets the current view action and returns that entry in the action_fields dict:
def get_action_config(self):
"""
Return the configuration in the `Meta.action_fields` dictionary for this
view's action.
"""
view = getattr(self, 'context', {}).get('view', None)
action = getattr(view, 'action', None)
action_fields = getattr(self.Meta, 'action_fields', {})
I changed get_field_names of ModelSerializer:
From:
fields = getattr(self.Meta, 'fields', None)
exclude = getattr(self.Meta, 'exclude', None)
To:
action_config = self.get_action_config()
if action_config:
fields = action_config.get('fields', None)
exclude = action_config.get('exclude', None)
else:
fields = getattr(self.Meta, 'fields', None)
exclude = getattr(self.Meta, 'exclude', None)
Finally, I changed the get_extra_kwargs method:
From:
extra_kwargs = copy.deepcopy(getattr(self.Meta, 'extra_kwargs', {}))
To:
action_config = self.get_action_config()
if action_config:
extra_kwargs = copy.deepcopy(action_config.get('extra_kwargs', {}))
else:
extra_kwargs = copy.deepcopy(getattr(self.Meta, 'extra_kwargs', {}))
If you want to change what fields are displayed in the List vs Detail view, the only thing you can do is change the Serializer used. There's no field that I know of that lets you specify which fields of the Serializer gets used.
The field selection on you serializers should be working, but I don't know what might be happening exactly. I have two solutions you can try:
1 Try to change the way you declare you serializer object
#If you aren't using Response:
from rest_framework.response import Response
class PersonListViewSet(viewsets.ModelViewSet):
def get(self, request):
queryset = Person.objects.all()
serializer_class = PersonListSerializer(queryset, many=True) #It may change the things
return Response(serializer_class.data)
class PersonViewSet(viewsets.ModelViewSet):
def get(self, request, pk): #specify the method is cool
queryset = Person.objects.all()
serializer_class = PersonSerializer(queryset, many=True) #Here as well
#return Response(serializer_class.data)
2 The second way around would change your serializers
This is not the most normal way, since the field selector should be working but you can try:
class PersonListSerializer(serializers.ModelSerializer):
nickname = serializers.SerializerMethodField() #Will get the attribute my the var name
slug = serializers.SerializerMethodField()
class Meta:
model = Person
def get_nickname(self, person):
#This kind of method should be like get_<fieldYouWantToGet>()
return person.nickname
def get_slug(self, person):
#This kind of method should be like get_<fieldYouWantToGet>()
return person.slug
I hope it helps. Try to see the APIview class for building your view too.
Somehow close:
If you just want to skip fields in the serilaizer
class UserSerializer(serializers.ModelSerializer):
user_messages = serializers.SerializerMethodField()
def get_user_messages(self, obj):
if self.context.get('request').user != obj:
# do somthing here check any value from the request:
# skip others msg
return
# continue with your code
return SystemMessageController.objects.filter(user=obj, read=False)
I rewrite ModelViewSet list function to modify serializer_class.Meta.fields attribute, code like this:
class ArticleBaseViewSet(BaseViewSet):
def list(self, request, *args, **kwargs):
exclude = ["content"]
self.serializer_class.Meta.fields = [f.name for f in self.serializer_class.Meta.model._meta.fields if f.name not in exclude]
queryset = self.filter_queryset(self.get_queryset()).filter(is_show=True, is_check=True)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class BannerArticleViewSet(ArticleBaseViewSet):
queryset = BannerArticle.objects.filter(is_show=True, is_check=True).all()
serializer_class = BannerArticleSerializer
permission_classes = (permissions.AllowAny,)
But it looks not stable, so i will not use it, just share to figure out the best way
My solution.
class BaseSerializerMixin(_ModelSerializer):
class Meta:
exclude: tuple[str, ...] = ()
exclude_in_list: tuple[str, ...] = ()
model: Type[_models.Model]
def get_action(self) -> Optional[str]:
if 'request' not in self.context:
return None
return self.context['request'].parser_context['view'].action
def get_fields(self):
fields = super().get_fields()
if self.get_action() == 'list':
[fields.pop(i) for i in list(fields) if i in self.Meta.exclude_in_list]
return fields
I think it should be like this:
router.register(r'persons/?P<slug>/', api.PersonViewSet)
and you should include a line like this:
lookup_field='slug'
in your serializer class. Like this:
class PersonSerializer(serializers.ModelSerializer):
lookup_field='slug'
class Meta:
model = Person
fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')