How to handle pre-selection of related fields and ensure proper update of many-to-many relationships in Django REST Framework?
I am working on implementing a Role-Based Access Control using Django and Django Rest Framework. I want to create a role with a set of permissions through the DRF browsable API. Additionally, I need the functionality to update those permissions, including adding new ones and removing existing ones. When displaying a role in the browsable API, I want the associated permissions to be pre-selected for clarity, while also showing all other available permissions for easy addition.
What I have done so far
Here is a simplified version of my model
class BaseModel(models.Model):
pkid = models.BigAutoField(primary_key=True, editable=False)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=True)
class Meta:
abstract = True
class Role(BaseModel):
name = models.CharField(max_length=100)
class Permission(BaseModel):
name = models.CharField(max_length=100, unique=True)
description = models.TextField(null=True, blank=True)
class RolePermission(BaseModel):
role = models.ForeignKey(
Role, on_delete=models.CASCADE, related_name="role_permissions"
)
permission = models.ForeignKey(Permission, on_delete=models.CASCADE)
Here is my serializer
class RoleSerializer(serializers.ModelSerializer):
permissions = serializers.SlugRelatedField(
queryset=Permission.objects.all(), many=True, required=False, slug_field="name"
)
business = serializers.PrimaryKeyRelatedField(
queryset=Business.objects.all(), required=False, allow_null=True
)
class Meta:
model = Role
fields = ("id", "name", "is_default", "permissions", "business")
def to_representation(self, instance):
representation = super().to_representation(instance)
permissions = instance.role_permissions.all().values_list(
"permission__name", flat=True
)
representation["permissions"] = list(permissions)
return representation
def to_internal_value(self, data):
data = data.copy()
permissions_data = data.pop("permissions", [])
permissions_qs = Permission.objects.filter(name__in=permissions_data)
data["permissions"] = [permission.id for permission in permissions_qs]
return super().to_internal_value(data)
The preselection works and all but when I send an UPDATE request to modify the permissions, I am getting errors like:
{
"permissions": [
"Object with name=[UUID('e4ac49c0-093a-4281-96b0-0846917fc62b')] does not exist."
]
}
I fixed the error by rewriting my serializer to something like this
class RoleSerializer(serializers.ModelSerializer):
permissions = serializers.PrimaryKeyRelatedField(
queryset=Permission.objects.all(), many=True, required=False
)
business = serializers.PrimaryKeyRelatedField(
queryset=Business.objects.all(), required=False, allow_null=True
)
class Meta:
model = Role
fields = ("id", "name", "is_default", "permissions", "business")
def to_representation(self, instance):
representation = super().to_representation(instance)
permissions = instance.role_permissions.all().values_list(
"permission", flat=True
)
representation["permissions"] = list(permissions)
return representation
def to_internal_value(self, data):
data = data.copy()
permissions_data = data.pop("permissions", [])
internal_value = super().to_internal_value(data)
internal_value["permissions"] = permissions_data
return internal_value
but doing this affects the json returned, now instead of the permission name I get the IDs like this
{
"id": "0b3de100-82ef-4a02-b94e-659cb6dd4314",
"name": "neweee",
"is_default": false,
"business": null,
"permissions": [
1,
2
]
}
My question: How can I correctly pre-select related fields (permissions) as names in the DRF browsable API, while still being able to create/update with no errors?
Thank you
PrimaryKeyRelatedField
may represent the target of the relationship using its primary key, as the documentation said. here
SlugRelatedField
may be used to represent the target of the relationship using a field on the target. here
For example:
class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.SlugRelatedField(
many=True,
read_only=True,
slug_field='title'
)
class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']
Output:
{
'album_name': 'Dear John',
'artist': 'Loney Dear',
'tracks': [
'Airport Surroundings',
'Everything Turns to You',
'I Was Only Going Out',
...
]
}
Personal solution:
class PermissionSerializer(serializers.ModelSerializer):
class Meta:
model = Permission
fields = ['id', 'name']
use this serializer in your RoleSerializer
class RoleSerializer(serializers.ModelSerializer):
permissions = PermissionSerializer(many=True, required=False)
# remaining code here .....