Using DRF serializer to validate a list of dictionaries - django

How do I write a serializer that validates a list of dictionaries?
Sample payload being sent is:
"payment_discount": [
{
"discount_rule_id": 1,
"discount_rule_name": "10 day early payment",
"discount_earned": "298.00"
},
{
"discount_rule_id": 2,
"discount_rule_name": "Store discount",
"discount_earned": "5.50"
},
]
Taking from this SO answer and this:
class PaymentDiscountSerializer(serializers.DictField):
discount_rule_id = serializers.IntegerField(required=False)
discount_rule_name = serializers.CharField(max_length=50)
discount_earned = serializers.DecimalField(max_digits=10, decimal_places=3)
class PaymentDiscountListSerializer(serializers.ListField):
"""Serialize discount object"""
child = PaymentDiscountSerializer()
class PaymentSerializer(serializers.ModelSerializer):
payment_discount = PaymentDiscountListSerializer()
# Other model fields
With this, I can get access to the payment_discount object in the payload using serializer.data but unfortunately no validations are being done against the dictionary if e.g. the payload includes discount_earned value of that is not of type Decimal
using: Django==1.10.2 & djangorestframework==3.5.1

I think you can simplify your serializers...
Haven't tested it but you could use something like this:
class PaymentDiscountSerializer(serializers.Serializer):
"""Serialize discounts"""
discount_rule_id = serializers.IntegerField(required=False)
discount_rule_name = serializers.CharField(max_length=50)
discount_earned = serializers.DecimalField(max_digits=10, decimal_places=3)
class PaymentSerializer(serializers.ModelSerializer):
payment_discount = PaymentDiscountSerializer(many=True)
# Other model fields
It should give you a list of object like you want.
For the validation, it should work right out of the box like this.
But again, I haven't tested it. If you are having problems with your validations, you can define your own.
Example:
class PaymentDiscountSerializer(serializers.Serializer):
"""Serialize discounts"""
discount_rule_id = serializers.IntegerField(required=False)
discount_rule_name = serializers.CharField(max_length=50)
discount_earned = serializers.DecimalField(max_digits=10, decimal_places=3)
def validate_discount_rule_id(self, value):
# Validation logic of the discount_rule_id field
#
return value
def validate(self, attrs):
# Validation logic of all the fields
#
return attrs
see http://www.django-rest-framework.org/api-guide/serializers/#field-level-validation for more infos.

Related

Django (drf) add new parameters/fields only for output with existing queryset(used for output) from database

I am working on a project on drf, where I need to fetch data from database (in thousands), and display selective fields in response.
models.py
class TblDemo(models.Model):
tbl_id = models.IntegerField()
tbl_name = models.CharField(max_length=50)
tbl_size = models.CharField(max_length=50)
tbl_height = models.Charfield(max_length=50)
tbl_material = models.Charfield(max_length=50)
class Meta:
managed = True
db_table = 'tbl_demo'
views.py
class data_fetch(viewsets.ViewSet):
def tbl_list(self, request, format=None)
serializer = Tbl_Serializer_input(data=request.data)
if serializer.is_valid():
queryset1 = tbl_name.objects.all()
queryset2 = queryset1.filter(tbl_name=request.data.get("title"))
serializer = Tbl_Serializer_output(queryset2, many=True)
return Response(serializer.data)
serializers.py
class Tbl_Serializer_input(serializers.Serializer):
title = serializers.CharField(required=True, max_length=50)
refer_no = serializers.CharField(required=True, max_length=50)
class Tbl_Serializer_output(serializers.Serializer):
tbl_id = serializers.CharField(max_length=50)
tbl_name = serializers.CharField(max_length=50)
tbl_size = serializers.CharField(max_length=50)
tbl_height = serializers.Charfield(max_length=50)
output
[
{
"tbl_id":"1",
"tbl_name":"white table",
"tbl_size": "square",
"tbl_height": "2 feet"
},
{
"tbl_id":"2",
"tbl_name":"black table",
"tbl_size": "square",
"tbl_height": "3 feet"
},
.......and so on.
but now, requirement is that I can't change database/model, but need to add some more fields like ("refer_no", "material" and "density", which is overall same in every cases) and with every object in queryset, which will not be stored in database, but it is only for response/output.
so, after adding new parameters my output will be like :
where, "refer_no" is taken directly from input to show output fields.
and "material" and "density" can't be added in database, just need to be hardcoded in middle.
new_output
[
{
"tbl_id":"1",
"refer_no":"abadadf",
"tbl_name":"white table",
"tbl_size": "square",
"tbl_height": "2 feet",
"density": "350gm/in"
"material": "tek wood"
},
{
"tbl_id":"2",
"refer_no":"abadadf",
"tbl_name":"black table",
"tbl_size": "square",
"tbl_height": "3 feet",
"density": "350gm/in",
"material": "tek wood"
},
.......and so on.
I have actually added new fields with query set from database, but when it went for serialization, it is showing circular loop error.
Please help.
You could process the data inside the serializer using the to_representation method.
For instance:
class ModelSerializer(serializers.ModelSerializer):
...
def to_representation(self, instance):
to_repr = super().to_representation(instance)
# add here the "refer_no", "material" and "density" key-value per object
return to_repr

Django filter exact match for multi field: ManyToManyField using ModelMultipleChoiceFilter

I'm using Django filters (django-filter) in my project. I have the models below, where a composition (Work) has a many-to-many instrumentations field with a through model. Each instrumentation has several instruments within it.
models.py:
class Work(models.Model):
instrumentations = models.ManyToManyField(Instrument,
through='Instrumentation',
blank=True)
class Instrument(models.Model):
name = models.CharField(max_length=100)
class Instrumentation(models.Model):
players = models.IntegerField(validators=[MinValueValidator(1)])
work = models.ForeignKey(Work, on_delete=models.CASCADE)
instrument = models.ForeignKey(Instrument, on_delete=models.CASCADE)
views.py:
import django_filters
class WorkFilter(django_filters.FilterSet):
instrument = django_filters.ModelMultipleChoiceFilter(
field_name="instrumentation__instrument",
queryset=Instrument.objects.all())
My filter works fine: it grabs all the pieces where there is the instrument selected by the user in the filter form.
However, I'd like to add the possibility of filtering the compositions with those exact instruments. For instance, if a piece contains violin, horn and cello and nothing else, I'd like to get that, but not a piece written for violin, horn, cello, and percussion. Is it possible to achieve that?
I'd also like the user to choose, from the interface, whether to perform an exact search or not, but that's a secondary issue for now, I suppose.
Update: type_of_search using ChoiceFilter
I made some progress; with the code below, I can give the user a choice between the two kinds of search. Now, I need to find which query would grab only the compositions with that exact set of instruments.
class WorkFilter(django_filters.FilterSet):
# ...
CHOICES = {
('exact', 'exact'), ('not_exact', 'not_exact')
}
type_of_search = django_filters.ChoiceFilter(label="Exact match?", choices=CHOICES, method="filter_instruments")
def filter_instruments(self, queryset, name, value):
if value == 'exact':
return queryset.??
elif value == 'not_exact':
return queryset.??
I know that the query I want is something like:
Work.objects.filter(instrumentations__name='violin').filter(instrumentations__name='viola').filter(instrumentations__name='horn')
I just don't know how to 'translate' it into the django_filters language.
Update 2: 'exact' query using QuerySet.annotate
Thanks to this question, I think this is the query I'm looking for:
from django.db.models import Count
instrument_list = ['...'] # How do I grab them from the form?
instruments_query = Work.objects.annotate(count=Count('instrumentations__name')).filter(count=len(instrument_list))
for instrument in instrument_list:
instruments_query = instruments_query.filter(instrumentations__name=instrument_list)
I feel I'm close, I just don't know how to integrate this with django_filters.
Update 3: WorkFilter that returns empty if the search is exact
class WorkFilter(django_filters.FilterSet):
genre = django_filters.ModelChoiceFilter(
queryset=Genre.objects.all(),
label="Filter by genre")
instrument = django_filters.ModelMultipleChoiceFilter(
field_name="instrumentation__instrument",
queryset=Instrument.objects.all(),
label="Filter by instrument")
CHOICES = {
('exact', 'exact'), ('not_exact', 'not_exact')
}
type_of_search = django_filters.ChoiceFilter(label="Exact match?", choices=CHOICES, method="filter_instruments")
def filter_instruments(self, queryset, name, value):
instrument_list = self.data.getlist('instrumentation__instrument')
if value == 'exact':
queryset = queryset.annotate(count=Count('instrumentations__name')).filter(count=len(instrument_list))
for instrument in instrument_list:
queryset = queryset.filter(instrumentations__name=instrument)
elif value == 'not_exact':
pass # queryset = ...
return queryset
class Meta:
model = Work
fields = ['genre', 'title', 'instrument', 'instrumentation']
You can grab instrument_list with self.data.getlist('instrument').
This is how you would use instrument_list for the 'exact' query:
type_of_search = django_filters.ChoiceFilter(label="Exact match?", choices=CHOICES, method=lambda queryset, name, value: queryset)
instrument = django_filters.ModelMultipleChoiceFilter(
field_name="instrumentation__instrument",
queryset=Instrument.objects.all(),
label="Filter by instrument",
method="filter_instruments")
def filter_instruments(self, queryset, name, value):
if not value:
return queryset
instrument_list = self.data.getlist('instrument') # [v.pk for v in value]
type_of_search = self.data.get('type_of_search')
if type_of_search == 'exact':
queryset = queryset.annotate(count=Count('instrumentations')).filter(count=len(instrument_list))
for instrument in instrument_list:
queryset = queryset.filter(instrumentations__pk=instrument)
else:
queryset = queryset.filter(instrumentations__pk__in=instrument_list).distinct()
return queryset

Returning array of deleted objects in single GraphQL mutation (graphene-django)

While in the past I used a GraphQL mutation that deletes a single record, I came up with the idea that this is not ideal in the case I want to delete several records at once since it ended up by calling the mutation that delete 1 record several times (in a loop) which results in several API call over the network.
So I decided to instead modify the mutation to accept has an argument a list of objects or IDs (whatever) on which I can loop in the backend. By doing this, it does only 1 call to the API with all the records to delete.
I managed to do it and the only blocking point that I face is the return statement. I couldn't figure out how to write it.
So if I have a model (in my schema) such as :
class UserType(DjangoObjectType):
class Meta:
model = User
fields = (
'id',
'username',
'password',
'first_name',
'last_name',
'is_active',
'group_ids',
)
full_name = graphene.String()
full_identification = graphene.String()
In the past I used :
class DeleteUser(graphene.Mutation):
username = graphene.String()
class Arguments:
id = graphene.ID()
def mutate(self, info, **kwargs):
user = get_object_or_404(User, id=kwargs['id'])
user.delete()
return user
class Mutation(graphene.ObjectType):
delete_user = DeleteUser.Field()
But now I want to do something such as :
class DeleteUsers(graphene.Mutation):
users = graphene.List(UserType)
username = graphene.String()
class Arguments:
ids = graphene.List(graphene.ID)
def mutate(self, info, **kwargs):
deleted_users = []
for user in User.objects.filter(id__in=kwargs['ids']):
user.delete()
deleted_users.append(user)
return deleted_users
class Mutation(graphene.ObjectType):
delete_users = DeleteUsers.Field()
You can see in the mutate(..) method of DeleteUsers class that I try to return the deleted_users to be able to do something like : "Users X, Y and Z have been deleted" in the frontend.
But I haven't been able to retrieve the data of the users. How can I achieve it ? Maybe I'm missing something.
Currently the GraphQL query I tried for that is :
mutation {
deleteUsers(
ids: [5,6,7],
) {
users {
username
}
}
}
but it doesn't work, saying users is null... Don't know how I could retrieve the users in the query.
Thanks in advance.
Well I finally figured out what was the problem (seems talking about a problem solves it, next time I'll try with a plastic duck).
While the deleted_users was a correct list with the Users, the ouput type of my mutation wasn't set.
So the solution is to add :
class Meta:
output = graphene.List(UserType)
in my mutation
which results in :
class DeleteUsers(graphene.Mutation):
class Arguments:
ids = graphene.List(graphene.ID)
class Meta:
output = graphene.List(UserType)
def mutate(self, info, **kwargs):
deleted_users = []
for user in User.objects.filter(id__in=kwargs['ids']):
user.delete()
deleted_users.append(user)
return deleted_users
Which can be called with the following :
mutation {
deleteUsers(
ids: [5,6,7],
) {
username
firstName
# ...
# And other fields ...
# ...
}
}

Django- filtering of the filter object

I want to make a complex filtering on the page using the FilterSets. This is my Filterset, nicely showing me tuples from chosen time and with chosen parameters.
# filters.py
class workFilter(filters.FilterSet):
start__gt = filters.DateTimeFilter(name='time_start', lookup_expr='gte')
start__lt = filters.DateTimeFilter(name='time_end', lookup_expr='lte')
class Meta:
model = Work
fields = ('machine', 'program')
But I want to add charts explaining the queried data. For that I need informations, like overall count of time. I am querying them like that:
#views.py
def search(request):
work_list = Work.objects.all()
work_filter = workFilter(request.GET, queryset=work_list)
filter_backends = (filters.DjangoFilterBackend,)
#some queries to add to context, such as
sum_work = Work.objects.aggregate(Sum('time'))['time__sum']
return render_to_response(
TEMPLATE_DIRS + 'index.html',
{
'filter': praca_filter,
'sum_work': sum_work,
}
)
But sadly, those queries are according to whole database, not to my filtered set of object.
How can I make queries on filtered set work_filter?
Define sum_work as a property of your FilterSet.
class WorkFilter(filters.FilterSet):
start__gt = filters.DateTimeFilter(name='time_start', lookup_expr='gte')
start__lt = filters.DateTimeFilter(name='time_end', lookup_expr='lte')
class Meta:
model = Work
fields = ('machine', 'program')
#property
def work_sum(self):
qs = super(WorkFilter, self).qs
return qs.aggregate(Sum('time'))['time__sum']
Then when you pass your filter through to your view you just need {{ filter.work_sum }} in your template.

serializer.data is missing some of the data

Context: I am having problem accessing fields which are validated by nested serializers.
I have a very sample model as shown below.
For 2 of the fields I have their specific serializers. When I try to access the data it returns all the fields except the one validated by the specific serializers.
Models looks like this
class Sampler(models.Model):
sample_name = models.CharField(unique=True, max_length=100)
sampling_by = JSONField(max_length=100)
extractions = JSONField(max_length=100)
max_samples = models.IntegerField(default=100)
Serializers
class ExtractionsSerializer(serializers.BaseSerializer):
table_name = serializers.CharField()
extraction_type = serializers.ChoiceField(["ABC"])
dependencies = serializers.ListField(child=RecursiveField(), allow_empty=True, required=False)
class SamplingBySerializer(serializers.BaseSerializer):
"""
Validate sampling_by
"""
def to_internal_value(self, samples):
methods = ["ABC"]
sampling_not_supported = [sample for sample in samples
if sample['by'] not in methods]
if sampling_not_supported:
raise ValidationError("{} not in {}".format(sampling_not_supported, methods))
class SamplerSerializer(serializers.ModelSerializer):
extractions = ExtractionsSerializer(read_only=True)
sampling_by = SamplingBySerializer(read_only=True)
class Meta:
model = Sampler
fields = ('sample_name', 'database', 'schema', 'sampling_by', 'extractions', 'max_samples')
Now I do
data = {
"database": "postgresql://..",
"sampling_by":[{
"by":"ABC",
"value": ["l32=turn_the"]
}],
"max_samples":3,
"extractions" : [{
"table_name": "person",
"extraction_type": "ABC"
}]
}
sampler = SamplerSerializer(data=data)
sampler.is_valid() #returns True
sampler.data => does not contain data of the nested fields. Like the `sampling_by` and `extractions`. Contains all other fields
sampler.validated_data => same problem as above
Any help would be appreciated! thanks
You probably missed the fact that your nested serializers are flagged as read_only=True
class SamplerSerializer(serializers.ModelSerializer):
extractions = ExtractionsSerializer(read_only=True)
sampling_by = SamplingBySerializer(read_only=True)
Remove that part, implement the serializer's create / update and you're good to go.
On a side note, it doesn't make sense to access serializer.data when deserializing.
Edit: the authority source is validated_data.