I'm trying to use Django Rest to return a json representation of a model based on a ordering from a custom field that is not attached to the model, but is attached to the serializer. I know how to do this with model specific fields, but how do you use django rest to return an ordering when the field is only within the serializer class? I want to return a list of Pics ordered by 'score'. Thanks!
------Views.py
class PicList(generics.ListAPIView):
queryset = Pic.objects.all()
serializer_class = PicSerializerBasic
filter_backends = (filters.OrderingFilter,)
ordering = ('score')
------Serializer.py
class PicSerializer(serializers.ModelSerializer):
userprofile = serializers.StringRelatedField()
score = serializers.SerializerMethodField()
class Meta:
model = Pic
fields = ('title', 'description', 'image', 'userprofile', 'score')
order_by = (('title',))
def get_score(self, obj):
return Rating.objects.filter(picc=obj).aggregate(Avg('score'))['score__avg']
You could move the logic of the method get_score to the manager of the class Pic. In this answer there is an example of how to do it.
Once you have it in the manager, the score field would become "magically" available for every object of the class Pic everywhere (serializer, views...) and you'll be able to use it for ordering.
Related
SOLUTION AT THE BOTTOM
Problem: Django form populating with list of objects rather than values
Summary: I have 2 models Entities and Breaks. Breaks has a FK relationship to the entity_id (not the PK) on the Entities model.
I want to generate an empty form for all the fields of Breaks. Generating a basic form populates all the empty fields, but for the FK it generates a dropdown list of all objects of the Entities table. This is not helpful so I have excluded this in the ModelForm below and tried to replace with a list of all the entity_ids of the Entities table. This form renders as expected.
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
exclude = ('entity',)
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all().values_list('entity_id', flat=True))
The below FormView is the cbv called by the URL. As the below stands if I populate the form, and for the FK column entity_id choose one of the values, the form will not submit. By that field on the form template the following message appears Select a valid choice. That choice is not one of the available choices.
class ContactFormView(FormView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
My initial thoughts were either that the datatype of this field (string/integer) was wrong or that Django needed the PK of the row in the Entities table (for whatever reason).
So I added a post function to the FormView and could see that the request.body was populating correctly. However I can't work out how to populate this into the ModelForm and save to the database, or overcome the issue mentioned above.
Addendum:
Models added below:
class Entity(models.Model):
pk_securities = models.AutoField(primary_key=True)
entity_id = models.CharField(unique=True)
entity_description = models.CharField(blank=True, null=True)
class Meta:
managed = False
db_table = 'entities'
class Breaks(models.Model):
pk_break = models.AutoField(primary_key=True)
date = models.DateField(blank=True, null=True)
entity = models.ForeignKey(Entity, on_delete= models.CASCADE, to_field='entity_id')
commentary = models.CharField(blank=True, null=True)
active = models.BooleanField()
def get_absolute_url(self):
return reverse(
"item-update", args=[str(self.pk_break)]
)
def __str__(self):
return f"{self.pk_break}"
class Meta:
managed = False
db_table = 'breaks'
SOLUTION
Firstly I got this working by adding the following to the Entity Model class. However I didn't like this as it would have consequences elsewhere.
def __str__(self):
return f"{self.entity_id}"
I found this SO thread on the topic. The accepted answer is fantastic and the comments to it are helpful.
The solution is to subclass ModelChoiceField and override the label_from_instance
class EntityChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return obj.entity_id
I think your problem is two fold, first is not rendering the dropdown correctly and second is form is not saving. For first problem, you do not need to do any changes in ModelChoiceField queryset, instead, add to_field_name:
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all(), to_field_name='entity_id')
Secondly, if you want to save the form, instead of FormView, use CreateView:
class ContactFormView(CreateView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
model = Breaks
In Django, the request object passed as parameter to your view has an attribute called "method" where the type of the request is set, and all data passed via POST can be accessed via the request. POST dictionary. The view will display the result of the login form posted through the loggedin. html.
I have multiple API classes (of ModelViewSet) that are inheriting from one common ViewSet.
So there are different kinds of models with their own fields but all share the same features that are defined in the common viewset.
One of those features - is filtering. It looks like this:
class CommonViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend,)
filter_fields = '__all__'
class FirstViewSet(CommonViewSet):
model = FirstModel
class SecondViewSet(CommonViewSet):
model = SecondModel
# etc...
Each model has a different set of fields, except that each model has a common field user. I don't want this field to be exposed in any way.
I have excluded this field from the serializers:
class CommonSerializer(serializers.ModelSerializer):
class Meta:
exclude = ('user',)
class SecondSerializer(CommonSerializer):
class Meta:
model = FirstModel
# etc...
What I want to achieve is to exclude the field user from filter_fields of CommonViewSet as well. I.e.:
filter_fields = '__all__' # except 'user'
Is there a standard way to do that?
Found the solution by myself.
To do so I defined CustomFilterBackend which expands the functionality of DjangoFilterBackend with filter_exclude:
class CustomFilterBackend(DjangoFilterBackend):
def get_filterset_class(self, view, queryset=None):
filterset = super().get_filterset_class(view, queryset)
filter_exclude = getattr(view, 'filter_exclude', None)
for x in filter_exclude:
filterset.base_filters.pop(x, None)
return filterset
So with such filter backend I can define on my ModelViewSet the additional field for excluded fields:
class UserCustomViewSet(viewsets.ModelViewSet):
filter_backends = (CustomFilterBackend,)
filter_fields = '__all__'
filter_exclude = ('user',)
This question already has answers here:
Django Rest Framework: Dynamically return subset of fields
(10 answers)
Closed 3 years ago.
For example, I have a Person model and its serializer
class Person(models.Model):
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
sex = models.IntegerField()
phone = models.CharField(max_length=255)
class SimplePersonSerializer(serializer.ModelSerializer):
class Meta:
model = Person
fields = ('first_name', 'last_name')
Then in my view function, I can:
#api_view(['GET'])
def people(request):
people = Person.objects.all()
data = SimplePersonSerializer(people, many=True).data
return Response(data)
However, when I profiler it using django-debug-toolbar, it shows that the serializer ask SQL Server to select all field of Person model, despite I only need first_name and last_name.
I know I can change people = Person.objects.all() to people = Person.objects.all().only('first_name', 'last_name') to make it. But I wonder if I can do this inside the serializer.
You can create dynamic field serializer for this and get the field data dynamically.
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(DynamicFieldsModelSerializer, self).__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.keys())
for field_name in existing - allowed:
self.fields.pop(field_name)
class SimplePersonSerializer(DynamicFieldsModelSerializer):
class Meta:
model = Person
fields = '__all__'
And then you can use it in your views like this.
#api_view(['GET'])
def people(request):
fields = ('first_name', 'last_name')
people = Person.objects.all().only(fields)
data = SimplePersonSerializer(people, many=True, fields = fields).data
return Response(data)
This helps to improve performance because it will fetch only the required data. (when using Person.objects.all().only('first_name', 'last_name') to fetch specific data)
You get all the fields queried because that's the query that runs by default when you do .all etcetera. You only limit the fields (SELECT field1, field2, ...) when you do .only, .values, or .values_list.
You can you can define the fields inside the serializer or you can go further and do dynamic things like: https://github.com/wimglenn/djangorestframework-queryfields
Inside the serializer:
class Meta:
fields = (*,...)
But, this is specific to the serializer. As the name implies this is just serializing the returned data into objects.
You can do queries in the serializer, but this typically for custom fields.
No you cannot achieve that by using builtin features of django and rest_framework.
Since serializer tries to access fields for model, you can describe properties by setting #property in your model or define custom SerializerMethodField, all this could possibly use all fields of your model.
I add a class method setup_eager_loading for SimplePersonSerializer
class SimplePersonSerializer(serializer.ModelSerializer):
#classmethod
def setup_eager_loading(cls, queryset):
queryset = queryset.only(*cls.Meta.fields)
return queryset
class Meta:
model = Person
fields = ('first_name', 'last_name')
And use it like this:
people = Person.objects.all()
people = SimplePersonSerializer.setup_eager_loading(people)
data = SimplePersonSerializer(people, many=True).data
I have two models first as parent model "Country", that filled before the second one as child model "City". as the following
class Country(models.Model):
name = models.CharField(max_length=35)
icon = models.ImageField()
def __str__(self):
return self.name
class City(models.Model):
name = models.CharField(max_length=35)
country = models.ForeignKey(to=Country, on_delete=models.CASCADE)
def __str__(self):
return self.name
My serializers.py for my need as following :
class CountrySerializer(ModelSerializer):
class Meta:
model = Country
fields = '__all__'
class CitySerializer(ModelSerializer):
country = serializers.PrimaryKeyRelatedField(queryset=Country.objects.all())
class Meta:
model = City
fields = ('name', 'country')
view.py
class CountryAPIView(ListAPIView):
queryset = Country.objects.all()
serializer_class = CountrySerializer
permission_classes = [AllowAny, AllowAnonymous]
class CityAPIView(ListAPIView):
queryset = City.objects.all()
serializer_class = CitySerializer
permission_classes = [AllowAny, AllowAnonymous]
def post(self, request):
serializer = CitySerializer(data=request.data)
if serializer.is_valid(raise_exception=ValueError):
serializer.create(validated_data=request.data)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.error_messages,
status=status.HTTP_400_BAD_REQUEST)
now when i run get api it run and gives me a result fine . But when im trying to create a new city and set "country":"id" in json i got this error
Cannot assign "2": "City.country" must be a "Country" instance.
So if i was not clear ,, what i need is exactly set foreign key to city when i create city ,, not create city and country,,
please any one had a solution help, because i tried many ways and read the django rest framework docs about this point but i didn't got it.
First of all, the raise_exception should be a boolean value (either True or False)
You could avoid this error by using inheriting the view class from ListCreateAPIView
from rest_framework.generics import ListCreateAPIView
class CityAPIView(ListCreateAPIView):
queryset = City.objects.all()
serializer_class = CitySerializer
permission_classes = [AllowAny, AllowAnonymous]
You don't want to use the post() method if you're using ListCreateAPIView, because DRF will take care of that part well.
Suggestion
Since you're dealing with CRUD functionalities of the model, you can use the DRF's ModelViewset class
you are not using the validated data to create a new city, just change this line:
serializer.create(validated_data=request.data)
to this:
serializer.save()
when you perform serializer.save(), the serializer will use its validated data.
also, DRF has a generic view(ListCreateAPIView) that covers your use-case.
I have a Product model and one propery in it is "my_test_fn". This is called from my serializer. My requirement is, I want to do some calculations based on the filter passing through the url. How can I get the url parameter values in a model property?
I want to get "filters" value in my_test_fn
models.py
class Product(AbstractProduct):
product_id = models.UUIDField(default=uuid.uuid4, editable=False)
##more fields to go
def my_test_fn(self):
filters = self.request.query_params.get('filters', None)
return {"key":"value"}
serializer.py
class MySerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('id','product_id','sku', 'title', 'my_test_fn',)
views.py
class ProductDetailConfiguration(viewsets.ViewSet):
lookup_field = 'product_id'
def retrieve(self, request, product_id=None):
queryset = Product.objects.filter(product_id=product_id)[0]
serializer = ProductConfigurationCustomSerializer(queryset, context={'request': request})
return Response(serializer.data)
API url:
http://127.0.0.1:8000/api/v1/product-configuration/2FC2AA43-07F5-DCF4-9A74-C840FDD8280A?filters=5
This logic belongs in the serializer, not the model. You can access it there via self.context['request'].
I guess what you want is not possible (have the my_fn on the model itself).
You would need to use a SerializerMethodField, so you will have access to the object, but to the request (and the various parameters of it) as well.