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
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 a question regarding django rest framework.
Most of the time, I have a serializer which has some read-only fields. For example, consider this simple model below:
class PersonalMessage(models.Model):
sender = models.ForeignKey(User, related_name="sent_messages", ...)
recipient = models.ForeignKey(User, related_name="recieved_messages", ...)
text = models.CharField(...)
def __str__(self) -> str:
return f"{self.text} (sender={self.sender})"
In this model, the value of sender and recipient should be automatically provided by the application itself and the user shouldn't be able to edit those fields. Alright, now take a look at this serializer:
class PersonalMessageSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
It perfectly prevents users from setting an arbitrary value on the sender and recipient fields. But the problem is, when these fields are marked as read-only in the serializer, the serializer will completely ignore all the values that are passed into the constructor for these fields. So when I try to create a model, no values would be set for these fields:
PersonalMessageSerializer(data={**request.data, 'sender': ..., 'recipient': ...) # Won't work
What's the best way to prevent users from setting an arbitrary value and at the same time auto-populate those restricted fields in django rest framework?
Depending on how you get those two objects, you can use the serializer's save method to pass them, and they will automatically be applied to the object you are saving:
sender = User.objects.first()
recipient = User.objects.last()
serializer = PersonalMessageSerializer(data=request.data)
message = serializer.save(sender=sender, recipient=recipient)
The kwargs should match the field names in your model for this to work. For reference, have a look here
You able to override the serializer context like this;
PersonalMessageSerializer(data={**request.data, context={'sender': sender, 'recipent': recipent})
and catch the context inside serializer.
class PersonalMessageSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
def validate(self, attrs):
attrs = super().validate(attrs)
attrs['sender'] = self.context['sender']
attrs['recipent'] = self.context['recipent']
return attrs
now serializer.validated_data it must returns sender and recipent.
From the question it is not possible to understand what field(s) of the relationship with sender and recipient you want to interact with, but a general answer can be found in the Serializer relations section of Django REST documentation.
Long story short, if you want to interact with one field only, you can use SlugRelatedField, which lets you interact with the target of the relationship using only one of its fields.
If it just the id, you can use PrimaryKeyRelatedField.
If you want to interact with more than one field, the way to go is Nested Relationships. Here you can specify a custom serializer for the target relationship, but you will have to override the create() method in your PersonalMessageSerializer to create the object from your relationship, as nested serializers are read-only by default.
So this is how you can make set a default on create but read only after in DRF. Although in this solution it wont actually be readonly, it's writable, but you now have explicit control on what the logged in user can write, which is the ultimate goal
Given the model
class PersonalMessage(models.Model):
sender = models.ForeignKey(User,...)
recipient = models.ForeignKey(User,..)
text = models.CharField(...)
You would first create your own custom default (I will show an example for only one field)
# Note DRF already has a CurrentUserDefault you can also use
class CurrentSenderDefault:
requires_context = True
def __call__(self, serializer_field):
return serializer_field.context['request'].user
def __repr__(self):
return '%s()' % self.__class__.__name__
Next you make your own field, that knows whats up with the filter.
This queryset prevents people from setting a value they are not allowed to. which is exactly what you want
class SenderField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
user = self.context['request'].user
if user:
queryset = User.objects.filter(id=user.id)
else:
queryset = User.objects.none()
return queryset
Finally on the serialiser you go
class PersonalMessageSerializer(serializers.ModelSerializer):
sender = SenderField(default=CurrentSenderDefault())
recipient = ...
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
I have a form that allows the user to pick several vans (many-to-many relationship). Each van has a boolean attribute named "available". I want to only show the vans whose "available" attribute is set to "True". How do I do this in the forms.py file?
I know that this could possibly be done in the template, but I did not want to create a new form-template with each individual field written out. I wanted to know if this functionality could be done in the forms.py file or in the class based view. I believe that doing it that way would be a bit cleaner. I've look into the validators but I don't think this is the way to go. Maybe I need to run a query set in the form file that checks the attribute before passing it to the form template?
views.py
def post(self, request):
"""Take in user data, clean it, and then post it to the database."""
form = self.form_class(request.POST) # pass in the user's data to that was submitted in form
if form.is_valid():
trip = form.save(commit=False) # create an object so we can clean the data before saving it
# now get the clean and normalize the data
first_name = form.cleaned_data['first_name']
last_name = form.cleaned_data['last_name']
trip_start = form.cleaned_data['trip_start']
trip_end = form.cleaned_data['trip_end']
van_used = form.cleaned_data['van_used']
trip.save()
forms.py
class TripForm(forms.ModelForm):
"""This class will be used to build trips."""
class Meta:
"""Specifying the database and fields to use."""
model = trips
fields = ['first_name', 'last_name', 'comments','trip_start', 'trip_end', 'van_used']
models.py
class trips(models.Model):
class Meta:
verbose_name_plural = "trips"
van_used = models.ManyToManyField(vans)
class vans(models.Model):
class Meta:
verbose_name_plural = "vans"
vanName = models.CharField(max_length=30, unique=True, blank=False)
available = models.BooleanField(default=True, blank=False)
# set up how the vans will be referenced in the admin page
def __str__(self):
return self.vanName
The final form that is rendered would only show the vans whose "available" attribute is set to True. Thanks in advance.
You have to override queryset for van_used field in form like this.
class TripForm(forms.ModelForm):
"""This class will be used to build trips."""
class Meta:
"""Specifying the database and fields to use."""
model = trips
fields = ['first_name', 'last_name', 'comments','trip_start', 'trip_end', 'van_used']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['van_used'].queryset = vans.objects.filter(available=True)
I am trying to enhance the django admin interface similar to what has been done in the accepted answer of this SO post. I have a many-to-many relationship between a User table and a Project table. In the django admin, I would like to be able to assign users to a project as in the image below:
It works fine with a simple ManyToManyField but the problem is that my model uses the through parameter of the ManyToManyField to use an intermediary table. I cannot use the save_m2m() and set() function and I am clueless on how to adapt the code below to make it work.
The model:
class UserProfile(models.Model):
user = models.OneToOneField(User, unique=True)
projects = models.ManyToManyField(Project, through='Membership')
class Project(models.Model):
name = models.CharField(max_length=100, unique=True)
application_identifier = models.CharField(max_length=100)
type = models.IntegerField(choices=ProjectType)
...
class Membership(models.Model):
project = models.ForeignKey(Project,on_delete=models.CASCADE)
user = models.ForeignKey(UserProfile,on_delete=models.CASCADE)
# extra fields
rating = models.IntegerField(choices=ProjectType)
...
The code used for the widget in admin.py:
from django.contrib.admin.widgets import FilteredSelectMultiple
class ProjectAdminForm(forms.ModelForm):
class Meta:
model = Project
fields = "__all__" # not in original SO post
userprofiles = forms.ModelMultipleChoiceField(
queryset=UserProfile.objects.all(),
required=False,
widget=FilteredSelectMultiple(
verbose_name='User Profiles',
is_stacked=False
)
)
def __init__(self, *args, **kwargs):
super(ProjectAdminForm, self).__init__(*args, **kwargs)
if self.instance.pk:
self.fields['userprofiles'].initial = self.instance.userprofile_set.all()
def save(self, commit=True):
project = super(ProjectAdminForm, self).save(commit=False)
if commit:
project.save()
if project.pk:
project.userprofile_set = self.cleaned_data['userprofiles']
self.save_m2m()
return project
class ProjectAdmin(admin.ModelAdmin):
form = ProjectAdminForm
...
Note: all the extra fields from the intermediary model do not need to be changed in the Project Admin view (they are automatically computed) and they all have a default value.
Thanks for your help!
I could find a way of solving this issue. The idea is:
Create new entries in the Membership table if and only if they are new (otherwise it would erase the existing data for the other fields in the Membership table)
Remove entries that were deselected from the Membership table
To do this, I replaced:
if project.pk:
project.userprofile_set = self.cleaned_data['userprofiles']
self.save_m2m()
By:
if project.pk:
# Get the existing relationships
current_project_selections = Membership.objects.filter(project=project)
current_selections = [o.userprofile for o in current_project_selections]
# Get the submitted relationships
submitted_selections = self.cleaned_data['userprofiles']
# Create new relation in Membership table if they do not exist
for userprofile in submitted_selections :
if userprofile not in current_selections:
Membership(project=project,userprofile=userprofile).save()
# Remove the relations that were deselected from the Membership table
for project_userprofile in current_project_selections:
if project_userprofile.userprofile not in submitted_selections :
project_userprofile.delete()
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.