I'm using Django 3, the Django REST framework, and Python 3.7. I have the following models. Notice that the second is dependent upon the first ...
class ContactMethod(models.Model):
class ContactTypes(models.TextChoices):
EMAIL = 'EMAIL', _('Email')
PHONE = 'PHONE', _('Phone')
type = models.CharField(
null=False,
max_length=5,
choices=ContactTypes.choices,
)
phone = PhoneNumberField(null=True)
email = models.EmailField(null=True)
class Meta:
unique_together = ('phone', 'email',)
...
class Coop(models.Model):
objects = CoopManager()
name = models.CharField(max_length=250, null=False)
types = models.ManyToManyField(CoopType)
addresses = models.ManyToManyField(Address)
enabled = models.BooleanField(default=True, null=False)
phone = models.ForeignKey(ContactMethod, on_delete=models.CASCADE, null=True, related_name='contact_phone')
email = models.ForeignKey(ContactMethod, on_delete=models.CASCADE, null=True, related_name='contact_email')
web_site = models.TextField()
I would like to submit some JSON to create my model, so I created the below two serializers to help me ...
class ContactMethodSerializer(serializers.ModelSerializer):
class Meta:
model = ContactMethod
fields = ['type', 'phone', 'email']
def create(self, validated_data):
contact_method = ContactMethod.objects.create(**validated_data)
return contact_method
def to_internal_value(self, data):
if type(data) == dict:
contatcmethod, created = CoopType.objects.create(**data)
# Replace the dict with the ID of the newly obtained object
data = contactmethod.pk
return super().to_internal_value(data)
...
class CoopSerializer(serializers.ModelSerializer):
types = CoopTypeSerializer(many=True)
addresses = AddressTypeField(many=True)
class Meta:
model = Coop
fields = ['id', 'name', 'types', 'addresses', 'phone', 'enabled', 'email', 'web_site']
def to_representation(self, instance):
rep = super().to_representation(instance)
rep['types'] = CoopTypeSerializer(instance.types.all(), many=True).data
rep['addresses'] = AddressSerializer(instance.addresses.all(), many=True).data
return rep
def create(self, validated_data):
#"""
#Create and return a new `Snippet` instance, given the validated data.
#"""
coop_types = validated_data.pop('types', {})
instance = super().create(validated_data)
for item in coop_types:
coop_type, _ = CoopType.objects.get_or_create(name=item['name'])
instance.types.add(coop_type)
return instance
The issue is, I'm not sure how to create my phone and email contact fields without submitting a primary key (I'd like to have that auto-generated). I created this test to try ...
def test_coop_create(self):
""" Test coop serizlizer model """
name = "Test 8899"
coop_type_name = "Library"
street = "222 W. Merchandise Mart Plaza, Suite 1212"
city = "Chicago"
postal_code = "60654"
enabled = True
postal_code = "60654"
email = EmailContactMethodFactory()
phone = PhoneContactMethodFactory()
web_site = "http://www.1871.com"
state = StateFactory()
serializer_data = {
"name": name,
"types": [
{"name": coop_type_name}
],
"addresses": [{
"formatted": street,
"locality": {
"name": city,
"postal_code": postal_code,
"state_id": state.id
}
}],
"enabled": enabled,
"phone": {
"phone": phone
},
"email": {
"email": email
},
"web_site": web_site
}
serializer = CoopSerializer(data=serializer_data)
serializer.is_valid()
assert serializer.is_valid(), serializer.errors
coop = serializer.save()
assert coop.name == name
type_count = 0
for coop_type in coop.types.all():
assert coop_type.name == coop_type_name
type_count = type_count + 1
assert type_count == 1
assert coop.addresses.first().locality.name == city
assert coop.addresses.first().locality.postal_code == postal_code
assert coop.addresses.first().locality.state.id == state.id
assert coop.enabled == enabled
assert coop.phone.phone == phone
assert coop.email.email == email
assert coop.web_site == web_site
but it results in the error below
AssertionError: {'phone': [ErrorDetail(string='Incorrect type. Expected pk value, received dict.', code='incorrect_type')], 'email': [ErrorDetail(string='Incorrect type. Expected pk value, received dict.', code='incorrect_type')]}
What's the correct way to set up my serializer to create the foreign keys without having to specify an ID?
Edit: GitHub repo:
https://github.com/chicommons/maps/tree/master/web
Out of box, rest-framework serializes relationships (phone
, email
) with primary keys.
Any relationships such as foreign keys on the model will be mapped to PrimaryKeyRelatedField. Reverse relationships are not included by default unless explicitly included as specified in the serializer relations documentation.
source: https://www.django-rest-framework.org/api-guide/serializers/#modelserializer
You would normally create ContactMethod
objects, one for name
(for instance with id=1) and second for email
(id=2), before creating Coop
and then include their id's in our payload. So it would look like
{
// ...
"phone": 1,
"email": 2,
// ...
}
In your case you need to create ContactMethod
when creating Coop
. You need to change CoopSerializer
to accept payload serialized by ContactMethodEmailSerializer
in email
field and ContactMethodPhoneSerializer
in phone
field.
class ContactMethodPhoneSerializer(serializers.ModelSerializer):
class Meta:
model = ContactMethod
fields = ['type', 'phone']
read_only_fields = ['type']
extra_kwargs = {'type': {'default': 'PHONE'}}
class ContactMethodEmailSerializer(serializers.ModelSerializer):
class Meta:
model = ContactMethod
fields = ['type', 'email']
read_only_fields = ['type']
extra_kwargs = {'type': {'default': 'EMAIL'}}
class CoopSerializer(serializers.ModelSerializer):
types = CoopTypeSerializer(many=True)
addresses = AddressTypeField(many=True)
phone = ContactMethodPhoneSerializer()
email = ContactMethodEmailSerializer()
With this serializers your test payload should be accepted.
In CoopSerializer.create
method you need to handle creation of ContactMethod
, similar to what you have done with CoopType
, you can follow example in docs:
https://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations