0

I'm encountering issues when trying to create a task with subtasks and users in my Django application. The task creation fails due to validation errors, specifically with the subtasks and users fields. Here is the payload I'm sending to the backend:

{
    "id": null,
    "title": "aa",
    "description": "aaaa",
    "due_to": "2024-06-27T22:00:00.000Z",
    "created": null,
    "updated": null,
    "priority": "LOW",
    "category": "TECHNICAL_TASK",
    "status": "TO_DO",
    "subtasks": [
        {
            "task_id": null,
            "description": "s1",
            "is_done": false
        },
        {
            "task_id": null,
            "description": "s2",
            "is_done": false
        }
    ],
    "users": [
        {
            "id": 6
        },
        {
            "id": 7
        }
    ]
}

When I attempt to create the task, I receive the following error message from the backend:

{
    "subtasks": [
        {
            "task_id": [
                "This field may not be null."
            ]
        },
        {
            "task_id": [
                "This field may not be null."
            ]
        }
    ],
    "users": [
        {
            "email": [
                "This field is required."
            ],
            "password": [
                "This field is required."
            ],
            "name": [
                "This field is required."
            ]
        },
        {
            "email": [
                "This field is required."
            ],
            "password": [
                "This field is required."
            ],
            "name": [
                "This field is required."
            ]
        }
    ]
}

I suspect the issue is related to how I'm handling the nested serializers and the model relationships in my Django application. I am using Django Rest Framework (DRF) to handle the serialization and deserialization of data.

Here is my relevant code:

serializers.py

class UserSerializer(serializers.ModelSerializer):
    """Serializer for the user object."""

    class Meta:
        model = get_user_model()
        fields = ['id', 'email', 'password', 'name', 'phone_number', 'avatar_color']
        extra_kwargs = {'password': {'write_only': True, 'style': {'input_type': 'password'}, 'min_length': 6},
                        'avatar_color': {'read_only': True}
                        }

    def create(self, validated_data):
        """Create and return a user with encrypted password."""
        return get_user_model().objects.create_user(**validated_data)

    def update(self, instance, validated_data):
        """Update and return user."""
        password = validated_data.pop('password', None)
        user = super().update(instance, validated_data)

        if password:
            user.set_password(password)
            user.save()

        return user


class AuthTokenSerializer(serializers.Serializer):
    """Serializer for the user auth token."""
    email = serializers.EmailField()
    password = serializers.CharField(
        style={'input_type': 'password'},
        trim_whitespace=False,
    )

    def validate(self, attrs):
        """Validate and authenticate the user."""
        email = attrs.get('email')
        password = attrs.get('password')
        user = authenticate(
            request=self.context.get('request'),
            username=email,
            password=password,
        )
        if not user:
            msg = _('Unable to authenticate with provided credentials.')
            raise serializers.ValidationError(msg, code='authorization')

        attrs['user'] = user
        return attrs




class TaskSerializer(serializers.ModelSerializer):
    """Serializes a task object"""
    subtasks = SubtaskSerializer(many=True, required=False)
    users = UserSerializer(many=True, required=False)

    class Meta:
        model = Task
        fields = ['id', 'title', 'description', 'due_to', 'created', 'updated', 'priority', 'category', 'status',
                  'subtasks', 'users']
        read_only_fields = ['created', 'updated']

    def create(self, validated_data):
        users_data = validated_data.pop('users', None)
        subtasks_data = validated_data.pop('subtasks', None)
        task = Task.objects.create(**validated_data)

        if users_data:
            for user_data in users_data:
                user = User.objects.get(id=user_data['id'])
                task.users.add(user)

        if subtasks_data:
            for subtask_data in subtasks_data:
                subtask_data['task_id'] = task.id
                SubtaskSerializer().create(validated_data=subtask_data)
        return task
        
        
class SubtaskSerializer(serializers.ModelSerializer):
    task_id = serializers.IntegerField(write_only=True, required=False)

    class Meta:
        model = Subtask
        fields = ['id', 'task_id', 'description', 'is_done']
        read_only_fields = ['id']

    def create(self, validated_data):
        task_id = validated_data.pop('task_id', None)
        task = Task.objects.get(id=task_id)
        subtask = Subtask.objects.create(task=task, **validated_data)
        return subtask

models.py

class UserManager(BaseUserManager):
    """Manager for user"""

    def create_user(self, email, name, password=None, **extra_fields):
        """Create, save and return a new user."""
        if not email:
            raise ValueError('User must have an email address.')
        user = self.model(email=self.normalize_email(email), name=name, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, name, password):
        """Create and save a new superuser with given details"""
        user = self.create_user(email, name, password)
        user.is_superuser = True
        user.is_staff = True
        user.save(using=self._db)
        return user


class User(AbstractBaseUser, PermissionsMixin):
    """Database model for users in the system"""

    email = models.EmailField(unique=True)
    name = models.CharField(max_length=50)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    phone_number = models.CharField(max_length=20, blank=True, null=True)
    avatar_color = models.CharField(max_length=7)

    objects = UserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['name']

    def save(self, *args, **kwargs):
        if not self.pk:
            self.avatar_color = random.choice([
                '#FF5733', '#C70039', '#900C3F', '#581845',
                '#8E44AD', '#1F618D', '#008000', '#A52A2A', '#000080'
            ])
        super().save(*args, **kwargs)

    def get_full_name(self):
        """Retrieve full name for user"""
        return self.name

    def __str__(self):
        """Return string representation of user"""
        return self.email
        
        

class Task(models.Model):
    class Priority(models.TextChoices):
        LOW = "LOW"
        MEDIUM = "MEDIUM"
        URGENT = "URGENT"

    class Category(models.TextChoices):
        TECHNICAL_TASK = "TECHNICAL_TASK"
        USER_STORY = "USER_STORY"

    class TaskStatus(models.TextChoices):
        TO_DO = "TO_DO"
        AWAIT_FEEDBACK = "AWAIT_FEEDBACK"
        IN_PROGRESS = "IN_PROGRESS"
        DONE = "DONE"

    id = models.AutoField(primary_key=True)
    title = models.TextField()
    description = models.TextField(blank=True, null=True)
    due_to = models.DateTimeField()
    created = models.DateTimeField()
    updated = models.DateTimeField(auto_now_add=True)
    priority = models.TextField(choices=Priority.choices)
    category = models.TextField(choices=Category.choices)
    status = models.TextField(choices=TaskStatus.choices)
    users = models.ManyToManyField(User, blank=True, null=True, related_name='tasks')

    def save(self, *args, **kwargs):
        if not self.id:
            self.created = timezone.now()
        super().save(*args, **kwargs)

    def __str__(self):
        return f"{self.title}"




class Subtask(models.Model):
    """Subtask object."""
    id = models.AutoField(primary_key=True)
    description = models.TextField()
    is_done = models.BooleanField(default=False)
    task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name='subtasks')

    def __str__(self):
        return self.description

1 Answer 1

0

When you start over-writing create validate with drf, unless it's absolutely justified (quite niche) you are breaking patterns that DRF wants you to use.

In your case, DRF behaves differently with patch vs put vs post etc...

for nested relations you have to specify those nested serializer e.g.:

    Non-nested: task_id = serializers.IntegerField(write_only=True, required=False)
    Nested:     task = TaskSerializer(required=False)

Finally:

Write unittests to isolate and test functionality you are interested in, write a minimum vertical slice of what you want and test it, then scale it up, don't try doing it all at once.

One key takeaway is that unittests may seem tedious, but they always save time both in the short & long run because they avoid these kinds of headaches.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.