limit django fields queried in sql call to database by queryset - python

I have a table with a blob and would like to exclude it from being in the sql call to the database unless specifically called for. Out of the box django includes everything in the queryset. So far the only way I have found to limit the field is to add a function to the view get_queryset()
def filter_queryset_fields(request, query_model):
fields = request.query_params.get('fields')
if fields:
fields = fields.split(',')
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set([f.name for f in query_model._meta.get_fields()])
values = []
for field_name in existing & allowed:
values.append(field_name)
queryset = query_model.objects.values(*values)
else:
queryset = query_model.objects.all()
return queryset
class TestViewSet(DynamicFieldsMixin, viewsets.ReadOnlyModelViewSet):
queryset = models.TestData.objects.all()
serializer_class = serializers.TestSerializer
filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
filter_fields = ('id', 'frame_id', 'data_type')
def get_queryset(self):
return filter_queryset_fields(self.request, models.TestData)
and mixin to the serializer to limit the fields it checks
class DynamicFieldsMixin(object):
def __init__(self, *args, **kwargs):
super(DynamicFieldsMixin, self).__init__(*args, **kwargs)
if "request" in self.context and self.context['request'] is not None:
fields = self.context['request'].query_params.get('fields')
if fields:
fields = fields.split(',')
# 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 TestSerializer(DynamicFieldsMixin, rest_serializers.ModelSerializer):
class Meta:
model = models.TestData
fields = '__all__'
this seems like a lot of code for what it does. Is there an easier way?

Django already have this right out the box. Just use defer or only
Defer will allow you the exclude a set of fields from the queryset:
MyModel.objects.defer('field_i_want_to_exclude')
While only allows you to say what fields you want on the queryset:
MyModel.objects.only('field_i_want1', 'field_i_want2')

Id do something like this for single, NON nested objects. For nested properties, you'll need more logic.
class DynamicFieldListSerializer(serializers.ListSerializer):
def to_representation(self, data):
"""
Code is a copy of the original, with a modification between the
iterable and the for-loop.
"""
# Dealing with nested relationships, data can be a Manager,
# so, first get a queryset from the Manager if needed
iterable = data.all() if isinstance(data, models.Manager) else data
fields = list(self.child.get_fields().keys())
iterable = iterable.only(*fields)
return [
self.child.to_representation(item) for item in iterable
]
class DynamicSerializerFieldsMixin:
def get_fields(self):
fields = super().get_fields()
raw_fields = set(self.context['request'].GET.get('fields', '').split(','))
# If querysparams ?fields= doesn't evaluate to anything, default to original
validated_fields = set(raw_fields) & set(fields.keys()) or set(fields.keys())
return {key: value for key, value in fields.items() if key in validated_fields}
#classmethod
def many_init(cls, *args, **kwargs):
meta = getattr(cls, 'Meta', None)
if not hasattr(meta, 'list_serializer_class'):
meta.list_serializer_class = DynamicFieldListSerializer
return super().many_init(*args, **kwargs)
For examples see:
https://gist.github.com/kingbuzzman/d7859d9734b590e52fad787d19c34b52#file-django_field_limit-py-L207

Use values():
MyModel.objects.values('column1', 'column2')

Related

Create Django Serializer with default fields in Meta and query params

I'm trying to write a Serializer that would take dynamic fields and add them to the restricted number of fields specified in Meta, but it seems that there's no method to "add back" a field to the serializer once it's been created.
Dynamic Fields per Django documentation
class DynamicFieldsModelSerializer(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)
for field_name in existing - allowed:
self.fields.pop(field_name)
class BldgLocationAndFilters(DynamicFieldsModelSerializer):
latitude = fields.FloatField(source='lat_016_decimal')
longitude = fields.FloatField(source='long_017_decimal')
class Meta:
model = Bldg
fields = ('latitude', 'longitude')
I'd like to do something that would modify the DynamicFieldsModelSerializer such that fields can be appended to the set that has already been filtered down, but it looks like the Meta fields override everything such that nothing can be added back (fields can only be removed
Pseudocode of desired behavior:
class DynamicFieldsUnionModelSerializer(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:
new_fields = set(fields)
existing = set(self.fields)
unique_new = new_fields.union(existing) - existing
for field_name in unique_new:
self.fields.update(field_name)
If BldgLocationAndFilters was called as serializer = BldLocationAndFilters(fields=['type']), I'd expect the resulting returns to have fields = ('latitude', 'longitude', 'type')
DynamicFieldsModelSerializer only works on removing existing fields, because the implementation depends on fields already built in __init__. You can add fields after the __init__, but you have to build them again somehow (not just add names).
But one way to support this is to override the serializer's get_field_names method which works with unbuilt field names:
class BldgLocationAndFilters(ModelSerializer):
latitude = fields.FloatField(source='lat_016_decimal')
longitude = fields.FloatField(source='long_017_decimal')
class Meta:
model = Bldg
fields = ('latitude', 'longitude')
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
self._fields_to_add = kwargs.pop('fields', None)
super().__init__(*args, **kwargs)
def get_field_names(self, *args, **kwargs):
original_fields = super().get_field_names(*args, **kwargs)
if self._fields_to_add:
return set(list(original_fields) + list(self._fields_to_add))
return original_fields
# Should use latitude, longitude, and type
BldgLocationAndFilters(instance, fields=('type',)).data
Note that this is using just ModelSerializer.
Or just define your serializer with __all__ (while still using DynamicFieldsModelSerializer) and set the fields on a per-need basis:
class BldgLocationAndFilters(DynamicFieldsModelSerializer):
latitude = fields.FloatField(source='lat_016_decimal')
longitude = fields.FloatField(source='long_017_decimal')
class Meta:
model = Bldg
fields = '__all__'
BldgLocationAndFilters(instance, fields=('latitude', 'longitude', 'type')).data

fetch ids with commas in django

view.py
def get(self, *args, **kwargs):
res = Question.objects.all()
# queryset = super().ListAPIView(*args, **kwargs)
if 'tag' in self.request.GET:
# split and directly put in main query
res = res.filter(
Tag_name=self.request.GET['tag']
)
if 'order_by' in self.request.GET:
res = res.order_by(
self.request.GET['order_by'])
serializer = QuestionSerializers(res, many=True)
return Response(serializer.data)
here I am trying to fetch question with some input tag ids and order by some input , how can I use multiple tag ids in url like
http://127.0.0.1:8000/?tag=1,2&order_by=name
so I get all objects with tag ids 1 and 2.
You can pass multiple get parameters with the same name:
http://127.0.0.1:8000/?tag=1&tag=2&order_by=name
In your view you can access the list using the getlist method:
def get(self, *args, **kwargs):
# returns empty list if no tags are provided
tags = self.request.GET.getlist('tag')
# you can set a default field to order_by, if no field is provided
order_by = self.request.GET.get('order_by', 'id')
# convert tags, currently strings, to int
tags = [int(tag) for tag in tags]
# not sure about this part would need to check your models
res = Question.objects.filter(tag__id__in=tags).order_by(order_by)
serializer = QuestionSerializers(res, many=True)
return Response(serializer.data)
You need to split values of tag by , and filter all tags with id__in filter like this
def get(self, *args, **kwargs):
res = Question.objects.all()
# queryset = super().ListAPIView(*args, **kwargs)
if self.request.GET.get('tag'):
# split and directly put in main query
res = res.filter(
tag_id__in=self.request.GET.get('tag').split(',')
)
if self.request.GET.get('order_by'):
res = res.order_by(
self.request.GET['order_by'])
serializer = QuestionSerializers(res, many=True)
return Response(serializer.data)
Well you can have a workaround(or better to say more django rest framework oriented way to achieve this) using ListAPIView of DRF like:
from rest_framework.generics import ListAPIView
class TagListAPIView(ListAPIView):
serializer_class = QuestionSerializers
filter_backends = (OrderingFilter)
ordering_fields = ['name', ...any other fields you wanna put here as an option for ordering]
ordering = 'name' #setting the default ordering
def get_queryset(self):
ids = self.request.query_params.get('tag', None)
if not ids:
return Question.objects.none() #Empty queryset
return Question.objects.filter(id__in=ids.split(','))
Haven't tested the code but yeah it should be somewhere around this and this code does not do all the proper input handling if some garbage input is coming from user(which is the case manytimes).
You could write more strong code for this by incorporating django-filters and putting a id field in that filter. Which would give you all the error validation and handling.
I have provided a links to DRF's doc regarding filter, ordering and all. https://www.django-rest-framework.org/api-guide/filtering/#orderingfilter
Have a look, its great content.
Ping me if anything unclear.

Complex aggregation methods as single param in query string

I’m trying to design a flexible API with django REST. What I meant by this is to have basically any field filterable through a query string and in addition to that have a param in the query string that can denote some complex method to perform. Ok, here are the details:
views.py
class StarsModelList(generics.ListAPIView):
queryset = StarsModel.objects.all()
serializer_class = StarsModelSerializer
filter_class = StarsModelFilter
serializers.py
class StarsModelSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
class Meta:
model = StarsModel
fields = '__all__'
mixins.py
class DynamicFieldsMixin(object):
def __init__(self, *args, **kwargs):
super(DynamicFieldsMixin, self).__init__(*args, **kwargs)
fields = self.context['request'].query_params.get('fields')
if fields:
fields = fields.split(',')
# 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)
filters.py
class CSVFilter(django_filters.Filter):
def filter(self, qs, value):
return super(CSVFilter, self).filter(qs, django_filters.fields.Lookup(value.split(u","), "in"))
class StarsModelFilter(django_filters.FilterSet):
id = CSVFilter(name='id')
class Meta:
model = StarsModel
fields = ['id',]
urls.py
url(r’^/stars/$’, StarsModelList.as_view())
this give me the ability to construct query strings like so:
/api/stars/?id=1,2,3&fields=type,age,magnetic_field,mass
this is great I like this functionality, but there are also many custom aggregation/transformation methods that need to be applied to this data. What I would like to do is have an agg= param like so:
/api/stars/?id=1,2,3&fields=type,age,magnetic_field,mass,&agg=complex_method
or just:
/api/stars/?agg=complex_method
where defining the complex_method grabs the correct fields for the job.
I’m not exactly sure where to start and where to add the complex methods so I would really appreciate some guidance. I should also note the api is only for private use supporting a django application, its not exposed to the public.
Definitely would be good to see your MyModelList class but anyway my example as per https://docs.djangoproject.com/en/1.10/ref/class-based-views/base/
from django.http import HttpResponse
from django.views import View
class StarsModelList(generics.ListAPIView):
queryset = StarsModel.objects.all()
serializer_class = StarsModelSerializer
filter_class = StarsModelFilter
def complex_method(request):
# do smth to input parameters if any
return HttpResponse('Hello, World!')
def get(self, request, *args, **kwargs):
if request.GET.get('agg', None) == 'complex_method':
return self.complex_method(request)
return HttpResponse('Hi, World!')

Dynamically exclude or include a field in Django REST framework serializer

I have a serializer in Django REST framework defined as follows:
class QuestionSerializer(serializers.Serializer):
id = serializers.CharField()
question_text = QuestionTextSerializer()
topic = TopicSerializer()
Now I have two API views that use the above serializer:
class QuestionWithTopicView(generics.RetrieveAPIView):
# I wish to include all three fields - id, question_text
# and topic in this API.
serializer_class = QuestionSerializer
class QuestionWithoutTopicView(generics.RetrieveAPIView):
# I want to exclude topic in this API.
serializer_class = ExamHistorySerializer
One solution is to write two different serializers. But there must be a easier solution to conditionally exclude a field from a given serializer.
Have you tried this technique
class QuestionSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
remove_fields = kwargs.pop('remove_fields', None)
super(QuestionSerializer, self).__init__(*args, **kwargs)
if remove_fields:
# for multiple fields in a list
for field_name in remove_fields:
self.fields.pop(field_name)
class QuestionWithoutTopicView(generics.RetrieveAPIView):
serializer_class = QuestionSerializer(remove_fields=['field_to_remove1' 'field_to_remove2'])
If not, once try it.
Creating a new serializer is the way to go. By conditionally removing fields in a serializer you are adding extra complexity and making you code harder to quickly diagnose. You should try to avoid mixing the responsibilities of a single class.
Following basic object oriented design principles is the way to go.
QuestionWithTopicView is a QuestionWithoutTopicView but with an additional field.
class QuestionSerializer(serializers.Serializer):
id = serializers.CharField()
question_text = QuestionTextSerializer()
topic = TopicSerializer()
class TopicQuestionSerializer(QuestionSerializer):
topic = TopicSerializer()
You can set fields and exclude properties of Meta
Here is an Example:
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
class Meta:
model = User
exclude = ['id', 'email', 'mobile']
def __init__(self, *args, **kwargs):
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
# #note: For example based on user, we will send different fields
if self.context['request'].user == self.instance.user:
# Or set self.Meta.fields = ['first_name', 'last_name', 'email', 'mobile',]
self.Meta.exclude = ['id']
Extending above answer to a more generic one
class QuestionSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
fields = kwargs.pop('fields', None)
super(QuestionSerializer, self).__init__(*args, **kwargs)
if fields is not None:
allowed = set(fields.split(','))
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
class QuestionWithoutTopicView(generics.RetrieveAPIView):
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
fields = self.request.GET.get('display')
serializer_class = self.get_serializer_class()
return serializer_class(fields=fields,*args, **kwargs)
def get_serializer_class(self):
return QuestionSerializer
Now we can give a query parameter called display to output any custom display format http://localhost:8000/questions?display=param1,param2
You can use to representation method and just pop values:
def to_representation(self, instance):
"""Convert `username` to lowercase."""
ret = super().to_representation(instance)
ret.pop('username') = ret['username'].lower()
return ret
you can find them here
https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior

Pass extra arguments to Serializer Class in Django Rest Framework

I want to pass some arguments to DRF Serializer class from Viewset, so for I have tried this:
class OneZeroSerializer(rest_serializer.ModelSerializer):
def __init__(self, *args, **kwargs):
print args # show values that passed
location = rest_serializer.SerializerMethodField('get_alternate_name')
def get_alternate_name(self, obj):
return ''
class Meta:
model = OneZero
fields = ('id', 'location')
Views
class OneZeroViewSet(viewsets.ModelViewSet):
serializer_class = OneZeroSerializer(realpart=1)
#serializer_class = OneZeroSerializer
queryset = OneZero.objects.all()
Basically I want to pass some value based on querystring from views to Serializer class and then these will be allocate to fields.
These fields are not include in Model in fact dynamically created fields.
Same case in this question stackoverflow, but I cannot understand the answer.
Can anyone help me in this case or suggest me better options.
It's very easy with "context" arg for "ModelSerializer" constructor.
For example:
in view:
my_objects = MyModelSerializer(
input_collection,
many=True,
context={'user_id': request.user.id}
).data
in serializers:
class MyModelSerializer(serializers.ModelSerializer):
...
is_my_object = serializers.SerializerMethodField('_is_my_find')
...
def _is_my_find(self, obj):
user_id = self.context.get("user_id")
if user_id:
return user_id in obj.my_objects.values_list("user_id", flat=True)
return False
...
so you can use "self.context" for getting extra params.
Reference
You could in the YourView override get_serializer_context method like that:
class YourView(GenericAPIView):
def get_serializer_context(self):
context = super().get_serializer_context()
context["customer_id"] = self.kwargs['customer_id']
context["query_params"] = self.request.query_params
return context
or like that:
class YourView(GenericAPIView):
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.context["customer_id"] = request.user.id
serializer.context["query_params"] = request.query_params
serializer.is_valid(raise_exception=True)
...
and anywhere in your serializer you can get it. For example in a custom method:
class YourSerializer(ModelSerializer):
def get_alternate_name(self, obj):
customer_id = self.context["customer_id"]
query_params = self.context["query_params"]
...
To fulfill the answer of redcyb - consider using in your view the get_serializer_context method from GenericAPIView, like this:
def get_serializer_context(self):
return {'user': self.request.user.email}
A old code I wrote, that might be helpful- done to filter nested serializer:
class MySerializer(serializers.ModelSerializer):
field3 = serializers.SerializerMethodField('get_filtered_data')
def get_filtered_data(self, obj):
param_value = self.context['request'].QUERY_PARAMS.get('Param_name', None)
if param_value is not None:
try:
data = Other_model.objects.get(pk_field=obj, filter_field=param_value)
except:
return None
serializer = OtherSerializer(data)
return serializer.data
else:
print "Error stuff"
class Meta:
model = Model_name
fields = ('filed1', 'field2', 'field3')
How to override get_serializer_class:
class ViewName(generics.ListAPIView):
def get_serializer_class(self):
param_value = self.context['request'].QUERY_PARAMS.get('Param_name', None)
if param_value is not None:
return Serializer1
else:
return Serializer2
def get_queryset(self):
.....
Hope this helps people looking for this.
List of element if your query is a list of elements:
my_data = DataSerializers(queryset_to_investigate,
many=True, context={'value_to_pass': value_passed}
in case off single data query:
my_data = DataSerializers(queryset_to_investigate,
context={'value_to_pass': value_passed}
Then in the serializers:
class MySerializer(serializers.ModelSerializer):
class Meta:
fields = '__all__'
model = 'Name_of_your_model'
def on_representation(self, value):
serialized_data = super(MySerializer, self).to_representation(value)
value_as_passed = self.context['value_to_pass']
# ..... do all you need ......
return serialized_data
As you can see printing the self inside on_representation you can see: query_set: <object (x)>, context={'value_to_pass': value_passed}
This is a simpler way, and you can do this in any function of serializers having self in the parameter list.
These answers are far to complicated; If you have any sort of authentication then add this property to your serializer and call it to access the user sending the request.
class BaseSerializer(serializers.ModelSerializer):
#property
def sent_from_user(self):
return self.context['request'].user
Getting the context kwargs passed to a serializer like;
...
self.fields['category'] = HouseCategorySerializer(read_only=True, context={"all_fields": False})
...
In your serializer, that is HouseCategorySerializer do this in one of your functions
def get_houses(self, instance):
print(self._context.get('all_fields'))
Using self._context.get('keyword') solved my mess quickly, instead of using self.get_extra_context()

Categories