0

These are 2 models I have:


class Skill(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name + " - ID: " + str(self.id)

class Experience(models.Model):
    consultant = models.ForeignKey("Consultant", related_name="experience", on_delete=models.CASCADE)
    project_name = models.CharField(max_length=100)
    company = models.CharField(max_length=100)
    company_description = models.TextField(null=True, blank=True)
    from_date = models.DateField()
    to_date = models.DateField()
    project_description = models.CharField(max_length=100)
    contribution = models.TextField()
    summary = models.TextField()
    is_pinned = models.BooleanField(default=False)
    role = models.CharField(max_length=100, null=True)
    skill = models.ForeignKey("Skill", related_name="experience", on_delete=models.CASCADE)

I want to do something that is quite common but apparently not possible out of the box with DRF: I want to have an endpoint /experience/ with a POST method where I can send a LIST of skill ids (skill field, ForeignKey). For example:

{
  "project_name": "Project AVC",
  "company": "XYZ Company",
  "company_description": "Description of XYZ Company",
  "from_date": "2022-01-01",
  "to_date": "2022-12-31",
  "project_description": "Description of Project ABC",
  "contribution": "Contributions to Project ABC",
  "summary": "Summary of Experience",
  "is_pinned": false,
  "role": "Consultant",
  "skills_ids": [1,2,3],
  "consultant": 1
}

If there are Skill records in the DB with ids 1,2,3 then it will create 3 records in the experience table (one for each skill ofc) . If there's no skill with such id, then during validation it should return an error to the user informing so.

The name of the field can be either skill , skills, skill_ids... it does not matter.

This is the ExperienceSerializer I created:

class ExperienceSerializer(serializers.ModelSerializer):
    skills = serializers.PrimaryKeyRelatedField(
        many=True,
        queryset=Skill.objects.all(),
        write_only=True
    )

    class Meta:
        model = Experience
        exclude = ['skill']

    def create(self, validated_data):
        skills_data = validated_data.pop('skills', [])
        experience = Experience.objects.create(**validated_data)

        for skill in skills_data:
            experience.skill.add(skill)

        return experience

but that gives me the error:

django.db.utils.IntegrityError: null value in column "skill_id" of relation "coody_portfolio_experience" violates not-null constraint DETAIL: Failing row contains (21, BOOM, XYZ Company, 2022-01-01, 2022-12-31, Description of Project ABC, Contributions to Project ABC, Summary of Experience, 1, null, f, Consultant, Description of XYZ Company).

I also tried using serializers.ListField but it doesn't seem to be quite the serializer for this.

Tried the approach from this answer as well, so then I had my serializer like this:

class ExperienceSerializer(serializers.ModelSerializer):
    skill_ids = serializers.ListField(
        child=SkillSerializer(),
        write_only=True
    )

    class Meta:
        model = Experience
        fields = (
            'consultant',
            'project_name',
            'company',
            'company_description',
            'from_date',
            'to_date',
            'project_description',
            'contribution',
            'summary',
            'is_pinned',
            'role',
            'skill',
            'skill_ids'
        )

    def create(self, validated_data):
        skill_ids = validated_data.pop('skill_ids')
        experience = Experience.objects.create(**validated_data)
        experience.set(skill_ids)

        return experience

I modified the answer a bit from child = serializers.IntegerField, to child=SkillSerializer(), as it was giving me an error of child not being instantiated. Noticed also the use of ListField now as well.

And here is my payload in this version:

{
 "project_name": "BOOM",
 "company": "XYZ Company",
 "company_description": "Description of XYZ Company",
 "from_date": "2022-01-01",
 "to_date": "2022-12-31",
 "project_description": "Description of Project ABC",
 "contribution": "Contributions to Project ABC",
 "summary": "Summary of Experience",
 "is_pinned": false,
 "role": "Consultant",
 "skill_ids": [3, 4,2,1],
   "consultant": 1
}

which gives error 400:

{
    "skill": [
        "This field is required."
    ],
    "skill_ids": {
        "0": {
            "non_field_errors": [
                "Invalid data. Expected a dictionary, but got int."
            ]
        },
        "1": {
            "non_field_errors": [
                "Invalid data. Expected a dictionary, but got int."
            ]
        },
        "2": {
            "non_field_errors": [
                "Invalid data. Expected a dictionary, but got int."
            ]
        },
        "3": {
            "non_field_errors": [
                "Invalid data. Expected a dictionary, but got int."
            ]
        }
    }
}

Tried also this example here to no avail. Spend some time reading this entire post explaining the issue of nested serialization, but I don't think it's quite related to my issue. All I want is a list to be sent in POST

I'm honestly going into a rabbit hole now of just trying different pieces together, but I have no idea how DRF wants me to do these stuff and their documentation is awful and lacking simple examples.

If someone could post example but also with explanations and not just the solution that would be much appreciated

Rafael Santos
  • 293
  • 3
  • 18

1 Answers1

1

With the current relation, if your payload contains "skills_ids": [1,2,3], then you would create three differrent instances of Experience each one containing a skill, which is NOT what you want, that is bad practice.

Instead, a many-to-many relationship is more adequate, associating multiple skills to an Experience and the other way around, thus avoiding duplicate values in your database.

Which is also the syntax that you are using at experience.skill.add(skill) that is how you would attach a Skill to an Experience using such relation. But, in reality you do not need to do anything other than letting the framework work for you!

models.py

class Skill(models.Model):
    ...


class Experience(models.Model):
    ...
    skills = models.ManyToManyField(Skill)

serializers.py

class ExperienceSerializer(serializers.ModelSerializer):
    class Meta:
        model = Experience
        fields = '__all__'

payload

{
  "project_name": "Project AVC",
  "company": "XYZ Company",
  "company_description": "Description of XYZ Company",
  "from_date": "2022-01-01",
  "to_date": "2022-12-31",
  "project_description": "Description of Project ABC",
  "contribution": "Contributions to Project ABC",
  "summary": "Summary of Experience",
  "is_pinned": false,
  "role": "Consultant",
  "skills": [1,2,3],
  "consultant": 1
}
Niko
  • 3,012
  • 2
  • 8
  • 14
  • this is why I was having a hard time finding a solution to the problem, because the entire premise and modelling was wrong. I can't believe I didn't see that basic mistake. Thank you so much for taking me out of my hole of misery :) – Rafael Santos Jun 01 '23 at 18:16