Here is my MSV:
models.py
import mongoengine
class PersonAddressModel(mongoengine.DynamicEmbeddedDocument):
country = mongoengine.fields.StringField()
town = mongoengine.fields.StringField()
class PersonModel(mongoengine.DynamicDocument):
name = mongoengine.fields.StringField()
age = mongoengine.IntField()
is_married = mongoengine.fields.BooleanField()
address = EmbeddedDocumentListField(PersonAddressModel)
serializers.py
from rest_framework_mongoengine import serializers
from .models import PersonInfoModel, PersonAddressModel
import mongoengine
class PersonAddressSerializer(serializers.EmbeddedDocumentSerializer):
class Meta:
model = PersonAddressModel
fields = '__all__'
class PersonSerializer(serializers.DynamicDocumentSerializer):
class Meta:
model = PersonModel
fields = '__all__'
views.py
from rest_framework_mongoengine import viewsets
from .serializers import PersonSerializer, PersonAddressSerializer
from rest_framework.response import Response
from rest_framework import status
import djongo
from .models import PersonModel
class PersonView(viewsets.ModelViewSet):
lookup_field = 'id'
serializer_class = PersonSerializer
def create(self, request):
serializer = self.serializer_class(data=request.data)
try:
serializer.is_valid()
serializer.save()
except djongo.sql2mongo.SQLDecodeError:
return Response(
status=status.HTTP_503_SERVICE_UNAVAILABLE
)
return Response(
status=status.HTTP_201_CREATED
)
I'm sending the following json object:
{
"name": "Helmut",
"age": 21,
"is_married": true,
"address": [{"country": "Germany", "town": "Berlin", "street": "Wolfstraße 1"}]
}
However when I check the database I see the following object:
{
"_id" : ObjectId("5b5e201c540d1c3b7a4491e8"),
"name" : "Helmut",
"age" : 21,
"is_married" : true,
"address" : [
{
"country" : "Germany",
"town" : "Berlin"
}
]
}
That's to say, Helmut's street is missing. I wonder why! My bet is that it has something to do with the serializers but I can't figure out what it is exactly.
EDIT
I want both my documents to be dynamic because some jsons may contain fields with unpredictable names and values and I have to save them as well, so I make my both model classes dynamic. However I can expand the PersonInfoModel but can't do the same with the PersonAddressModel. The street field won't show up in the DB (MongoDB).
On the other hand, I want some of the fields present in the address list to be required.
In your PersonAddressModel you defined only two fields, which are country and town. Which means, in DB, you defined the schema with those two fields, street is not included.
So, change your models to add extra field, as
class PersonAddressModel(mongoengine.DynamicEmbeddedDocument):
country = mongoengine.fields.StringField()
town = mongoengine.fields.StringField()
street = mongoengine.fields.StringField()
UPDATE
If your address field is dynamic and it's a dict like field, then use mongoengine.fields.DictField() as,
import mongoengine
class PersonModel(mongoengine.DynamicDocument):
name = mongoengine.fields.StringField()
age = mongoengine.IntField()
is_married = mongoengine.fields.BooleanField()
address = mongoengine.fields.DictField()
Update-2
You can do a field level validation in PersonSerializer as,
from rest_framework_mongoengine import serializers
from rest_framework import serializers as drf_serializer
class PersonSerializer(serializers.DynamicDocumentSerializer):
def validate_address(self, address):
required_fields = ['street', 'country']
if not all([True for field in required_fields if field in address]):
raise drf_serializer.ValidationError('some required fields are missing')
return address
class Meta:
model = PersonModel
fields = '__all__'
Since rest_framework_mongoengine doesnt have a ValidationError class, we using DRF's ValidationError class
UPDATE-3
Inorder to raise validation error, you have to pass True to .is_valid() (reff doc - Raising an exception on invalid data)method, as
class PersonView(viewsets.ModelViewSet):
lookup_field = 'id'
serializer_class = PersonSerializer
def create(self, request):
serializer = self.serializer_class(data=request.data)
try:
serializer.is_valid(True) # Change is here <<<<
serializer.save()
except djongo.sql2mongo.SQLDecodeError:
return Response(
status=status.HTTP_503_SERVICE_UNAVAILABLE
)
return Response(
status=status.HTTP_201_CREATED
)
Related
In one of my APIs, I am able to get the DRF extra field, PointField working.
In one of the other API where I am using PointField in a nested serializer, it is giving me a validation error.
{
"booking_address": {
"coordinates": [
"Enter a valid location."
]
}
}
And the payload data is
{
"booking_address": {
"coordinates" : {
"latitude": 49.87,
"longitude": 24.45
},
"address_text": "A123"
}
}
My serializers are below:
BookingSerializer
class BookingSerializer(FlexFieldsModelSerializer):
booked_services = BookedServiceSerializer(many=True)
booking_address = BookingAddressSerializer(required=False)
------
def validate_booking_address(self, address):
if address.get("id"):
address = BookingAddress.objects.get(id=address.get("id"))
else:
address["customer"] = self.context.get("request").user.id
serializer = BookingAddressSerializer(data=address)
if serializer.is_valid(): <---- error is coming from here
address = serializer.save()
else:
raise ValidationError(serializer.errors)
return address
My Address Serializer is defined as:
class BookingAddressSerializer(FlexFieldsModelSerializer):
coordinates = geo_fields.PointField(srid=4326)
customer = serializers.IntegerField(required=False)
And booking model is:
class BookingAddress(BaseTimeStampedModel):
customer = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="booking_addresses", on_delete=models.CASCADE)
coordinates = models.PointField()
address_text = models.CharField(max_length=256)
Tried Debugging, been stuck here for a few hours now and not able to find the issue.
Any help will be appreciated.
Well, the problem is that to_internal_value() is called with a correct Pointfield, and because geofields.PointField only handles strings or dicts it fails.
Here's the code I used to reproduce the problem (trimmed down, with imports):
# models.py
from __future__ import annotations
import typing as t
from django.contrib.gis.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
if t.TYPE_CHECKING:
from django.contrib.auth.models import User
class BaseTimeStampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
last_modified = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class BookingAddress(BaseTimeStampedModel):
customer = models.ForeignKey(
User, related_name="booking_addresses", on_delete=models.CASCADE,
)
coordinates = models.PointField(geography=True, srid=4326)
address_text = models.CharField(max_length=256)
class Booking(BaseTimeStampedModel):
service = models.CharField(max_length=20)
address = models.ForeignKey(
BookingAddress, on_delete=models.CASCADE, related_name="booking"
)
# serializers.py
import json
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from drf_extra_fields import geo_fields
from .models import BookingAddress, Booking
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.geos.error import GEOSException
from django.contrib.gis.geos.point import Point
EMPTY_VALUES = (None, "", [], (), {})
class PointField(geo_fields.PointField):
default_error_messages = {
"invalid": "Enter a valid location.",
"json": "Invalid json",
"unknown": "Unknown cause",
"wrong_type": "Expected string or dict",
}
def to_internal_value(self, value):
if value in EMPTY_VALUES and not self.required:
return None
if isinstance(value, str):
try:
value = value.replace("'", '"')
value = json.loads(value)
except ValueError:
self.fail("json")
print(type(value))
if value and isinstance(value, dict):
try:
latitude = value.get("latitude")
longitude = value.get("longitude")
return GEOSGeometry(
"POINT(%(longitude)s %(latitude)s)"
% {"longitude": longitude, "latitude": latitude},
srid=self.srid,
)
except (GEOSException, ValueError) as e:
msg = e.args[0] if len(e.args) else "Unknown"
self.fail(f"unknown", msg=msg)
if isinstance(value, Point):
raise TypeError("Point received")
self.fail(f"wrong_type")
class BookingAddressSerializer(serializers.ModelSerializer):
coordinates = PointField(srid=4326)
class Meta:
model = BookingAddress
fields = ("coordinates", "customer_id")
class BookingSerializer(serializers.ModelSerializer):
booking_address = BookingAddressSerializer(required=False)
def validate_booking_address(self, address):
if address.get("id"):
address = BookingAddress.objects.get("id")
else:
address["customer"] = self.context.get("request").user.id
serializer = BookingAddressSerializer(data=address)
if serializer.is_valid():
address = serializer.save()
else:
raise ValidationError(serializer.errors)
return address
class Meta:
model = Booking
fields = ("service", "created_at", "last_modified", "booking_address")
read_only_fields = ("created_at", "last_modified")
# tests.py
import json
from django.contrib.auth import get_user_model
from django.test import TestCase
from .serializers import BookingSerializer
User = get_user_model()
class DummyRequest:
user = None
class BookingSerializerTest(TestCase):
payload = json.dumps(
{
"service": "Dry cleaning",
"booking_address": {
"coordinates": {"longitude": 24.45, "latitude": 49.87},
"address_text": "123 Main Street",
},
}
)
def test_serializer(self):
user = User.objects.create_user(username="test_user", password="pass123456")
request = DummyRequest()
request.user = user
serializer = BookingSerializer(
data=json.loads(self.payload), context={"request": request}
)
self.assertTrue(serializer.is_valid(raise_exception=True))
If you run the test you see in the output:
<class 'dict'>
<class 'django.contrib.gis.geos.point.Point'>
And the 2nd time is where it fails, because it cannot handle a Point. This is caused by you overstepping your boundaries in validate_booking_address(). This causes to_internal_value to be called twice, the 2nd time with the result of the previous.
You're trying to handle the entire convert > validate > save operation there and the method should only do the validate step. This means, check to see if the data matches the expected input.
A nested field should validate itself and be able to create itself. If you need request.user to create a valid model you should override create() as explained by the documentation:
If you're supporting writable nested representations you'll need to write .create() or .update() methods that handle saving multiple objects.
Having the following Model:
class Book(models.Model):
name = models.CharField()
author = models.CharField()
date = models.DateField()
class Meta:
unique_together = ('name', 'author')
class BookSerializerWrite(serializers.ModelSerializer):
class Meta:
model = Book
class BookView(ApiView):
def put(self, request, *args, **kwargs):
serializer = BookSerializerWrite(data=request.data)
if serializer.is_valid():
serializer.save()
The view above does not work as the serializer.is_valid() is False.
The message is:
'The fields name, author must make a unique set'
Which is the constraint of the model.
How do I update the model?
I would rather not override the serializer's validation method.
I also cannot access the validated_data for an update as in
https://www.django-rest-framework.org/api-guide/serializers/#saving-instances
as this is empty due to the fact that the form does not validate.
Is there a builtin solution?
You can achieve it using UpdateAPIview
serializers.py
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('name', 'author', 'date')
views.py
from rest_framework.generics import UpdateAPIview
from .serializers import BookSerializer
class BookUpdateView(UpdateAPIView):
serializer_class = BookSerializer
urls.py
from django.urls import path
from . import views
url_patterns = [
path('api/book/<int:pk>/update/', views.BookUpdateView.as_view(), name="book_update"),
]
Now, post your data to above url. It should work.
Reference: https://github.com/encode/django-rest-framework/blob/master/rest_framework/generics.py
My models are:
class User(models.Model):
id = models.AutoField(primary_key=True)
email = models.EmailField()
class Lawyer(models.Model):
user = models.OnetoOneField(User)
class Session(models.Model):
name = models.CharField()
lawyer = models.ForeignKey(Lawyer)
I am trying to create multiple objects with a list serializer for Session.
From the app side they don't have the id of lawyer, but have the email of each lawyer. How can I write a list serializer where I can take the following json input and use email to fetch lawyer to store multiple session objects?
The json input sent will be like:
[
{
"name": "sess1",
"email": "lawyer1#gmail.com",
},
{
"name": "sess1",
"email": "lawyer1#gmail.com",
}
]
You can do it in this way but I think email should be unique=True.
Then use a serializer like this:
from django.utils.translation import ugettext_lazy as _
class SessionCreateManySerializer(serializers.ModelSerializer):
email = serializers.SlugRelatedField(
source='lawyer',
slug_field='user__email',
queryset=Lawyer.objects.all(),
write_only=True,
error_messages={"does_not_exist": _('lawyer with email={value} does not exist.')}
)
class Meta:
model = Session
fields = ('name', 'email')
and a generic create view (just override get_serializer and place many=True in kwargs ):
from rest_framework.response import Response
from rest_framework import generics
class SessionCreateManyView(generics.CreateAPIView):
serializer_class = SessionCreateManySerializer
def get_serializer(self, *args, **kwargs):
kwargs['many'] = True
return super(SessionCreateManyView, self).get_serializer(*args, **kwargs)
You can use bulk creation as well:
# serializers.py
from myapp.models import Session
from rest_framework import serializers
class SessionSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
name = serializers.CharField(required=True)
def validate_email(self, email):
lawyer = Lawyer.objects.filter(user__email=email).first()
if not lawyer:
raise ValidationError(detail="user dose not exist.")
return lawyer
def create(self, validated_data):
return Session.objects.create(name=validated_data.get('name'), lawyer=validated_data.get('email'))
and in your api.py file allow bulk creation:
# api.py
from rest_framework import generics
class SessionCreateAPIView(generics.CreateAPIView):
"""Allows bulk creation of a resource."""
def get_serializer(self, *args, **kwargs):
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
return super().get_serializer(*args, **kwargs)
I have a DRF ViewSet called "QueryCriteriaViewSet" which I'm using in a query builder which allows users to select a field and then select from related criteria. So, for example, a user can select the "reg_status" field and then select from the related criteria of "Active" and "Inactive".
This works totally fine when I select a field from the main "person" model. But I'm running into problems when I select a field from a related model like the "lookup_party" model. The weird thing though, is that when I print the queryset to the console it works perfectly, but when I get call the API it returns a list of empty objects.
As a further example, here's what happens when I make the calls:
api/querycriteria/?fields=reg_status returns:
[
{"reg_status": "Active"},
{"reg_status": "Inactive"}
]
while api/querycriteria/?fields=party__party_name returns:
[
{},
{},
{},
{},
{}
]
even though when I print(queryset) prior to returning the queryset, the following is printed:
<QuerySet [{'party__party_name': None}, {'party__party_name': 'Democratic'},
{'party__party_name': 'Non-Partisan'}, {'party__party_name': 'Registered
Independent'}, {'party__party_name': 'Republican'}]>
Here's the full ViewSet:
class QueryCriteriaViewSet(DefaultsMixin, viewsets.ModelViewSet):
serializer_class = QueryCriteriaSerializer
def get_queryset(self):
fields = self.request.GET.get('fields', None)
queryset = Person.objects.values(fields).distinct()
print(queryset)
return queryset
def get_fields_to_display(self):
fields = self.request.GET.get('fields', None)
return fields.split(',') if fields else None
def get_serializer(self, instance=None, data=empty, many=False,
partial=False):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
context = self.get_serializer_context()
fields = self.get_fields_to_display()
return serializer_class(instance, data=data,
many=many, partial=partial,
context=context, fields=fields)
Let me know if any additional information would be helpful.
Here's my serializer:
from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework.reverse import reverse
from ..models.people import Person
from ..models.people_emails import Email
from ..models.people_addresses import Address
from ..models.people_phones import Phone
from ..models.people_tags import PersonTag
from ..models.people_elections import PersonElection
from ..models.people_districts import PersonDistrict
from ..models.people_attributes import Attribute
from .serializer_dynamic_fields import DynamicFieldsModelSerializer
from .serializer_tag import TagSerializer
from .serializer_email import EmailSerializer
from .serializer_address import AddressSerializer
from .serializer_phone import PhoneSerializer
from .serializer_election import ElectionSerializer
from .serializer_attribute import AttributeSerializer
from .serializer_district import DistrictSerializer
class QueryCriteriaSerializer(DynamicFieldsModelSerializer):
emails = EmailSerializer(many=True, required=False)
addresses = AddressSerializer(many=True, required=False)
phones = PhoneSerializer(many=True, required=False)
tags = TagSerializer(many=True, required=False)
elections = ElectionSerializer(many=True, required=False)
attributes = AttributeSerializer(many=True, required=False)
districts = DistrictSerializer(many=True, required=False)
class Meta:
model = Person
fields = ('id', 'elected_official', 'title', 'first', 'last', 'middle', 'suffix',
'full_name', 'birthdate', 'sex', 'website', 'deceased', 'registered', 'party',
'reg_date', 'reg_status', 'reg_state', 'county', 'match_id',
'date_added', 'date_updated', 'do_not_call', 'do_not_mail', 'do_not_email', 'do_not_text', 'emails',
'addresses', 'phones', 'tags', 'attributes', 'elections', 'districts')
And here's the DynamicFieldsModelSerializer:
from django.contrib.auth import get_user_model
from rest_framework import serializers
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)
for field_name in existing - allowed:
self.fields.pop(field_name)
ok now found the problem, DynamicFieldsModelSerializer is only working if the fields you are passing to it is a subset of the serializer original fields.
you should use a serializers in a way that accepts extra fields, something like this:
class ExtraDynamicFieldsModelSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
extra_fields = kwargs.pop('fields', [])
self.extra_fields = set()
# Instantiate the superclass normally
super(ExtraDynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
allowed = set(extra_fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
for field_name in allowed - existing:
self.extra_fields.add(field_name)
def to_representation(self, obj):
data = super().to_representation(obj)
for field in self.extra_fields:
data[field] = obj[field]
return data
I want to Translate the variables "name" and "description" of my CategorySerializer, when serializing.
from rest_framework import serializers
from api.models import Category
from django.utils.translation import ugettext_lazy as _
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'image', 'description')
Is the serializer method field a good aproach?
PD: this names are translated on the django.po.
Yes, simply define a SerializerMethodField and return the translation on the fly. The following example changes the field 'description' to 'translation':
class CategorySerializer(serializers.ModelSerializer):
translation = SerializerMethodField('get_description_string')
class Meta:
model = Category
fields = ('id','translation',)
def get_description_string(self,obj):
return obj.description
serializers.py
from rest_framework import serializers
from api.models import Category
from django.utils.translation import ugettext_lazy as _
class CategorySerializer(serializers.ModelSerializer):
name_ = serializers.ReadOnlyField(source='get_name')
description_ = serializers.ReadOnlyField(source='get_description')
class Meta:
model = Category
fields = ('id', 'name', 'image', 'description')
def get_name(self):
return _(self.name)
def get_description(self):
return _(self.name)
If you want, you can change fields' name as "name" and "description". And then;
def to_representation(self, instance):
"""
Object instance -> Dict of primitive datatypes.
"""
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
# We skip `to_representation` for `None` values so that fields do
# not have to explicitly deal with that case.
#
# For related fields with `use_pk_only_optimization` we need to
# resolve the pk value.
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
if check_for_none is None:
ret[field.field_name] = None
else:
# override to_representation function
if field.field_name == "name" or field.field_name == "description":
ret[field.field_name] = _(attribute)
return ret