I have to serializer a list of email addresses. If one of them contains a wrong character (in one case, it was a ":" at the end of the address) my serializer throws an error, rejects serializing the whole set of addresses and returns a HTTP 400. Is there a way to "pop" the faulty email address from the list but still serialize the remaining correct addresses?
View:
#action(detail=False, methods=['post'])
def match(self, request):
serializer = FriendFinderSerializer(data=request.data, many=True)
if serializer.is_valid():
contacts = serializer.validated_data
matched, unmatched = self.match_contacts(contacts)
serialized_matched = FriendFinderSerializer(matched, many=True)
serialized_unmatched = FriendFinderSerializer(unmatched, many=True)
data = {
'matched': serialized_matched.data,
'unmatched': serialized_unmatched.data,
}
return Response(data, status=status.HTTP_200_OK)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Serializer:
class FriendFinderSerializer(serializers.Serializer):
id = serializers.IntegerField(required=False)
image = serializers.ImageField(required=False)
record_id = serializers.CharField()
phone_numbers = serializers.ListField(child=serializers.CharField(), required=False)
email_addresses = serializers.ListField(child=serializers.EmailField(), required=False)
relationship_id = serializers.CharField(required=False)
relationship_status = serializers.CharField(required=False)
A place to start would be to iterate over request.data and handle each element within a loop. This is definitely a break from the norm and you'll need to determine how to handle cases where there is good and bad data.
#action(detail=False, methods=['post'])
def match(self, request):
successful_data = []
error_data = []
for element in request.data:
serializer = FriendFinderSerializer(data=element, many=False)
if serializer.is_valid():
contacts = serializer.validated_data
matched, unmatched = self.match_contacts(contacts)
serialized_matched = FriendFinderSerializer(matched, many=True)
serialized_unmatched = FriendFinderSerializer(unmatched, many=True)
successful_data.append({
'matched': serialized_matched.data,
'unmatched': serialized_unmatched.data,
})
else:
error_data.append(serializer.errors)
# Determine what status to return and how to handle successes and errors.
Personally, I'd either make smaller requests rather than posting all the data or handle the case in which an error in one FriendFinderSerializer causes all to fail. What you're attempting to do is likely to cause you more pain than the other options.
This is kind of a hack, but it works.
You need to define a custom field and there you need to override to_internal_value method of the field. Like this:
class CustomField(serializers.Field):
def __init__(self, custom_validation, *args, **kwargs):
self.custom_validation=custom_validation # <-- Here pass your custom validation regex
super(CustomField, self).__init__(*args, **kwargs)
def to_representation(self, obj):
return obj
def to_internal_value(self, obj):
try:
match = re.search(self.custom_validation, obj) # <-- validate the value against regex
if match:
return obj
except Exception as e:
pass
return None # <-- If exception occurs, return None
Or override the EmailField like this:
class CustomEmailField(serializer.EmailField):
def run_validation(self,*args, **kwargs):
try:
return super(CustomEmailField, self).run_validation(*args,**kwargs) # <-- Runs validation, but it fails, returns None
except:
return None
And use it in the Serializer like this:
email = CustomField(r"(^[a-zA-Z0-9_.+-]+#[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)") # regex for email
Or
email = CustomEmailField()
And if you want to pop the invalid values from ListField, then you can override to_representation like this:
class CustomListField(serializers.ListField):
def to_representation(self, data):
childs = super(CustomListField, self).to_representation(data)
new_childs = []
for child in childs:
if child:
new_childs.append(child)
return new_childs
Then, use it in the Serializer:
email_addresses = CustomListField(child=CustomEmailField(), required=False)
Related
I am using a multipart/form-data in a form which have a manytomany relation as well as multiple file upload. But the validated data doesn't contains the array data
Views.py
class ExpenseCreateAPIView(CreateAPIView):
permission_classes = (permissions.IsAuthenticated,)
parser_classes = ( MultiPartParser,)
def post(self, request, *args, **kwargs):
owner = request.user.pk
d = request.data.copy()
d['owner'] = owner
serializer = ExpenseSerializer(data=d)
print("exp")
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Serializers.py
class ExpenseSerializer(serializers.ModelSerializer):
transactions = ExpenseTransactionsSerializer(many=True, required=False)
bill = ExpenseFilesSerializer(many=True, required=False)
class Meta:
model = Expense
fields = "__all__"
def create(self, validated_data):
print("validated data", validated_data)
items_objects = validated_data.pop('transactions', None)
files_objects = validated_data.pop('bill', None)
prdcts = []
files = []
for item in items_objects:
i = ExpenseTransactions.objects.create(**item)
prdcts.append(i)
if files_objects == None:
pass
else:
for item in files_objects:
i = ExpenseFiles.objects.create(**item)
files.append(i)
instance = Expense.objects.create(**validated_data)
instance.transactions.set(prdcts)
instance.bill.set(files)
return instance
How else should I use the MultiPartParser class in the views ?
I keep getting the error:
TypeError: 'NoneType' object is not iterable
at
for item in items_objects:
Make sure you have transactions in validated_data. If transactions can empty or not required, just change:
validated_data.pop('transactions', None)
to
validated_data.pop('transactions', [])
It means that if transactions not in validated_data then pop [] (an empty list), instead of None, then empty list can keep iterate in next your code.
I think you need to try validated_data.get instead of validated_data.pop
So I have a model like this.
class Member(BaseModel):
objects = models.Manager()
user = models.ForeignKey('api_backend.User', db_index=True, on_delete=models.CASCADE)
cluster = models.ForeignKey('api_backend.Cluster', on_delete=models.CASCADE)
And a generic api view for the same.
lass MemberPutRetrieveUpdateDeleteView(PutAsCreateMixin, MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView):
queryset = api_models.Member.objects.all()
permission_classes = [permissions.IsAuthenticated, IsMemberOrKickMembers]
lookup_fields = ['user', 'cluster']
def get_serializer_class(self):
if self.request.method in ['PUT']:
return api_serializers.PartialMemberSerializer
return api_serializers.MemberSerializer
def destroy(self, request, *args, **kwargs):
member = self.get_object()
if member.cluster.owner == member.user:
raise exceptions.ValidationError("cannot delete membership with this cluster as you own it.")
return super(MemberPutRetrieveUpdateDeleteView, self).destroy(request, *args, **kwargs)
I am currently using these mixins.
class PutAsCreateMixin(object):
"""
The following mixin class may be used in order to support PUT-as-create
behavior for incoming requests.
"""
def update(self, request, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object_or_none()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
if instance is None:
if not self.lookup_fields:
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup_value = self.kwargs[lookup_url_kwarg]
extra_kwargs = {self.lookup_field: lookup_value}
else:
# add kwargs for additional fields
extra_kwargs = {field: self.kwargs[field] for field in self.lookup_fields if self.kwargs[field]}
serializer.save(**extra_kwargs)
return Response(serializer.data, status=201)
serializer.save()
return Response(serializer.data)
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
def get_object_or_none(self):
try:
return self.get_object()
except Http404:
if self.request.method == 'PUT':
# For PUT-as-create operation, we need to ensure that we have
# relevant permissions, as if this was a POST request. This
# will either raise a PermissionDenied exception, or simply
# return None.
self.check_permissions(clone_request(self.request, 'POST'))
else:
# PATCH requests where the object does not exist should still
# return a 404 response.
raise
class MultipleFieldLookupMixin(object):
"""
Apply this mixin to any view or viewset to get multiple field filtering
based on a `lookup_fields` attribute, instead of the default single field filtering.
"""
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {}
for field in self.lookup_fields:
if self.kwargs[field]: # Ignore empty fields.
filter[field] = self.kwargs[field]
obj = get_object_or_404(queryset, **filter) # Lookup the object
self.check_object_permissions(self.request, obj)
return obj
So, in my serializer, I have multiple lookup fields -user and cluster. Both of these are foreign keys and have their own kwargs in the url.
So my api url is like this.
path('clusters/<int:cluster>/members/<int:user>/', views.MemberPutRetrieveUpdateDeleteView.as_view())
and I would expect a sample url to be like this:
'clusters/3/members/2/'
where 1 is the id of the cluster and 2 is the id of the member.
So basically a put request to this url must create a member which has:
an user foreign key of id 2
a cluster foreign key of id 3
But when trying to create the same with the mixin, I get the following error.
in __set__
self.field.remote_field.model._meta.object_name,
ValueError: Cannot assign "2": "Member.user" must be a "User" instance.
How can I fix this error? Can someone please help me?
thanks a lot!
The answer is actually very simple,
I hope this will save a lot of someone's time.
I had to change the lookup fields to the following
lookup_fields = ['user_id', 'cluster_id']
and the url kwargs to the following
'clusters/<int:cluster_id>/members/<int:user_id>/'
this way, django knows that only the id of the foreign key is present in the url, and not the foreign key object itself. When perform-creating with the lookup fields, everything works just as expected!
I have a TransactionType model and I've implemented a viewset method to create transaction type as shown also below. Currently I can only post single credit_account or debit_account items as shown in this payload:
{"name":"Repair and Maintenance","credit_account":16,"debit_account":38}
I would to post multiple credit_accounts and debit_accounts such that my payload looks something like this:
{"name":"Repair and Maintenance","credit_account":[16,4,5],"debit_account":[38,7]}
Which is the efficient way of do this?
class TransactionType(models.Model):
name = models.CharField(max_length=255)
organization = models.IntegerField(null=True, blank=False)
credit_account = models.ManyToManyField(Account,related_name='credit_account', verbose_name="Account to Credit")
debit_account = models.ManyToManyField(Account,related_name='debit_account',verbose_name="Account to Debit")
def __str__(self):
return '{}'.format(self.name)
viewset method
def create(self, request, format=None):
name = request.data['name']
try:
trans_type_obj = TransactionType.objects.create(name=name,
credit_account=Account.objects.get(id=request.data['credit_account'
]),
debit_account=Account.objects.get(id=request.data['debit_account'
]), organization=get_auth(request))
serializer = CreateTransactionTypeSerializer(trans_type_obj)
except Exception, e:
raise e
return Response(data=serializer.data,
status=status.HTTP_201_CREATED)
Use ManyToManyField.add() as below,
def create(self, request, format=None):
name = request.data['name']
try:
trans_type_obj = TransactionType.objects.create(name=name, organization=get_auth(request))
trans_type_obj.credit_account.add(*[credit_obj for credit_obj in Account.objects.filter(id__in=request.data['credit_account'])])
trans_type_obj.debit_account.add(*[debit_obj for debit_obj in Account.objects.filter(id__in=request.data['debit_account'])])
serializer = CreateTransactionTypeSerializer(trans_type_obj)
except Exception, e:
raise e
return Response(data=serializer.data,
status=status.HTTP_201_CREATED)
UPDATE-1
as #Daniel Roseman said, it's also possible to do the same without list comperhension as
trans_type_obj.credit_account.add(*Account.objects.filter(id__in=request.data['credit_account']))
trans_type_obj.debit_account.add(*Account.objects.filter(id__in=request.data['debit_account']))
def create(self, request,*args, **kwargs):
name = request.data.pop('name')
credits = request.data.pop('credit_account')
debits = request.data.pop('debit_account')
try:
trans_type_obj = TransactionType.objects.create(name=name, organization=get_auth(request))
for item in credits:
trans_type_obj.credit_account.add(item)
for item in debits:
trans_type_obj.debit_account.add(item)
serializer = TransactionTypeSerializer(trans_type_obj)
except Exception as e:
raise e
return Response(data=serializer.data, status=status.HTTP_201_CREATED)
you can create one serializer with following fields
class TransactionTypeSerializer(serializers.ModelSerializer):
credit_account = serializers.PrimaryKeyRelatedField(queryset=Account.objects.all(), many=True)
debit_account = serializers.PrimaryKeyRelatedField(queryset=Account.objects.all(), many=True)
class Meta:
model = TransactionType
fields = __all__
now in views
def create(self, request, *args, **kwargs):
serializer = TransactionTypeSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
I'm using Django rest framework, Here is my serializers.py for social app:
class SocialPostSerializer(serializers.ModelSerializer):
likes = serializers.SerializerMethodField() # define field
class Meta:
model = SocialPost
def get_likes(self, obj):
post_id = obj.id
#I get post_like from django-redis
post_like = get_redis_connection("default")
likes = post_like.get("post"+":"+str(post_id))
if likes == None:
return 0
else:
likes = likes.decode('utf-8')
return likes
With the code above, I got what I need from the API.
Since 'likes' doesn't exist in my database(Mysql here), I can't using order_by('likes') to sort the data with django ORM
I follow the doc here ListCreateAPIView which lead me to override list(): (I had override create() and get_queryset() before)
from operator import itemgetter
class PostList(generics.ListCreateAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Post.objects.all()
serializer_class = PostAllSerializer
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
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)
#problem here
serializer.data = sorted(serializer.data, key=itemgetter(serializer.data['likes']))
return Response(serializer.data)
def create(self, request, *args, **kwargs):
act_id = request.data.get('act')
act = Act.objects.get(pk=act_id)
if act.act_type == 0:
if request.user != act.user:
return Response(status=403)
return super().create(request, args, kwargs)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
def get_queryset(self):
queryset = Post.objects.all().order_by('-post_create_time')
act_id = self.request.query_params.get('act_id', None)
post_author = self.request.query_params.get('post_author', None)
if act_id is not None:
queryset = queryset.filter(act=act_id)
if post_author is not None:
queryset = queryset.filter(user__user_name=post_author)
return queryset
Nothing happened, What is wired is even when I uncomment
return Response(serializer.data)
Still nothing happened, Which part is wrong?
Another question is when I wanna add some extra data like 'question' when I use django FBV:
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': 'some data I wanna add'})
Is it possible to add the data in serializer.data? For example, I wanna display user_id from which user request this api:
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
user_id = request.user.id #how can I add the user_id into response data?
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
I look around the docs here Including extra context.
serializer = AccountSerializer(account, context={'request': request})
I don't really understand how to add data in it.
"Since 'likes' doesn't exist in my database(Mysql here), I can't using order_by('likes') to sort the data with django ORM"
You wont be able to sort the results using django ORM as the likes field is not a part of your DB table. The only way to do it is to sort the serializer.data that you get in your view. In your case serializer.data will be a list of dictionary, you can use sort command for list and sort on likes using lambda.
One thing to take care here is as you will be doing the sort by loading the data in memory, make sure that you dont load a lot of data. Have a check on memory utilization.
"Another question is when I wanna add some extra data like 'question' when I use django FBV"
I did'nt understand what is needed here.
I am not very experience with Django REST framework and have been trying out many things but can not make my PATCH request work.
I have a Model serializer. This is the same one I use to add a new entry and ideally I Would want to re-use when I update an entry.
class TimeSerializer(serializers.ModelSerializer):
class Meta:
model = TimeEntry
fields = ('id', 'project', 'amount', 'description', 'date')
def __init__(self, user, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
super(TimeSerializer, self).__init__(*args, **kwargs)
self.user = user
def validate_project(self, attrs, source):
"""
Check that the project is correct
"""
.....
def validate_amount(self, attrs, source):
"""
Check the amount in valid
"""
.....
I tried to use a class based view :
class UserViewSet(generics.UpdateAPIView):
"""
API endpoint that allows timeentries to be edited.
"""
queryset = TimeEntry.objects.all()
serializer_class = TimeSerializer
My urls are:
url(r'^api/edit/(?P<pk>\d+)/$', UserViewSet.as_view(), name='timeentry_api_edit'),
My JS call is:
var putData = { 'id': '51', 'description': "new desc" }
$.ajax({
url: '/en/hours/api/edit/' + id + '/',
type: "PATCH",
data: putData,
success: function(data, textStatus, jqXHR) {
// ....
}
}
In this case I would have wanted my description to be updated, but I get errors that the fields are required(for 'project'and all the rest). The validation fails. If add to the AJAX call all the fields it still fails when it haves to retrieve the 'project'.
I tried also to make my own view:
#api_view(['PATCH'])
#permission_classes([permissions.IsAuthenticated])
def edit_time(request):
if request.method == 'PATCH':
serializer = TimeSerializer(request.user, data=request.DATA, partial=True)
if serializer.is_valid():
time_entry = serializer.save()
return Response(status=status.HTTP_201_CREATED)
return Response(status=status.HTTP_400_BAD_REQUEST)
This did not work for partial update for the same reason(the validation for the fields were failing) and it did not work even if I've sent all the fields. It creates a new entry instead of editing the existing one.
I would like to re-use the same serializer and validations, but I am open to any other suggestions.
Also, if someone has a piece of working code (ajax code-> api view-> serializer) would be great.
class DetailView(APIView):
def get_object(self, pk):
return TestModel.objects.get(pk=pk)
def patch(self, request, pk):
testmodel_object = self.get_object(pk)
serializer = TestModelSerializer(testmodel_object, data=request.data, partial=True) # set partial=True to update a data partially
if serializer.is_valid():
serializer.save()
return JsonResponse(code=201, data=serializer.data)
return JsonResponse(code=400, data="wrong parameters")
Documentation
You do not need to write the partial_update or overwrite the update method. Just use the patch method.
Make sure that you have "PATCH" in http_method_names. Alternatively you can write it like this:
#property
def allowed_methods(self):
"""
Return the list of allowed HTTP methods, uppercased.
"""
self.http_method_names.append("patch")
return [method.upper() for method in self.http_method_names
if hasattr(self, method)]
As stated in documentation:
By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the partial argument in order to allow partial updates.
Override update method in your view:
def update(self, request, *args, **kwargs):
instance = self.get_object()
serializer = TimeSerializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save(customer_id=customer, **serializer.validated_data)
return Response(serializer.validated_data)
Or just override method partial_update in your view:
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
Serializer calls update method of ModelSerializer(see sources):
def update(self, instance, validated_data):
raise_errors_on_nested_writes('update', self, validated_data)
# Simply set each attribute on the instance, and then save it.
# Note that unlike `.create()` we don't need to treat many-to-many
# relationships as being a special case. During updates we already
# have an instance pk for the relationships to be associated with.
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
Update pushes the validated_data values to the given instance. Note that update should not assume all the fields are available. This helps to deal with partial updates (PATCH requests).
The patch method is worked for me using viewset in DRF. I'm changing you code:
class UserViewSet(viewsets.ModelViewSet):
queryset = TimeEntry.objects.all()
serializer_class = TimeSerializer
def perform_update(self, serializer):
user_instance = serializer.instance
request = self.request
serializer.save(**modified_attrs)
return Response(status=status.HTTP_200_OK)
Use ModelViewSet instead and override perform_update method from UpdateModelMixin
class UserViewSet(viewsets.ModelViewSet):
queryset = TimeEntry.objects.all()
serializer_class = TimeSerializer
def perform_update(self, serializer):
serializer.save()
# you may also do additional things here
# e.g.: signal other components about this update
That's it. Don't return anything in this method. UpdateModelMixin has implemented update method to return updated data as response for you and also clears out _prefetched_objects_cache. See the source code here.
I ran into this issues as well, I solved it redefining the get_serializer_method and adding custom logic to handle the partial update. Python 3
class ViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action == "partial_update":
return PartialUpdateSerializer
Note: you may have to override the partial_update function on the serializer. Like so:
class PartialUpdateSerializer(serializers.Serializer):
def partial_update(self, instance, validated_data):
*custom logic*
return super().update(instance, validated_data)
Another posiblity is to make the request by URL. For example, I have a model like this
class Author(models.Model):
FirstName = models.CharField(max_length=70)
MiddleName = models.CharField(max_length=70)
LastName = models.CharField(max_length=70)
Gender = models.CharField(max_length=1, choices = GENDERS)
user = models.ForeignKey(User, default = 1, on_delete = models.CASCADE, related_name='author_user')
IsActive = models.BooleanField(default=True)
class Meta:
ordering = ['LastName']
And a view like this
class Author(viewsets.ModelViewSet):
queryset = Author.objects.all()
serializer_class = AuthorSerializer
So can enter http://127.0.0.1:8000/author/ to get or post authors. If I want to make a PATCH request you can point to http://127.0.0.1:8000/author/ID_AUTHOR from your client. For example in angular2, you can have something like this
patchRequest(item: any): Observable<Author> {
return this.http.patch('http://127.0.0.1:8000/author/1', item);
}
It suppose you have configured your CORS and you have the same model in back and front.
Hope it can be usefull.