'How can I show the StringRelatedField instead of the Primary Key while still being able to write-to that field using Django Rest Framework?

Models:

class CrewMember(models.Model):

    DEPARTMENT_CHOICES = [
        ("deck", "Deck"),
        ("engineering", "Engineering"),
        ("interior", "Interior")
    ]

    first_name = models.CharField(max_length=25)
    last_name = models.CharField(max_length=25)
    email = models.EmailField()
    department = models.CharField(max_length=12, choices=DEPARTMENT_CHOICES)
    date_of_birth = models.DateField()
    join_date = models.DateField()
    return_date = models.DateField(null=True, blank=True)
    leave_date = models.DateField(null=True, blank=True)
    avatar = models.ImageField(null=True, blank=True)
    active = models.BooleanField(default=True)

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

class RosterInstance(models.Model):
    date = models.DateField(default=timezone.now)
    deckhand_watchkeeper = models.ForeignKey(CrewMember, on_delete=models.PROTECT, null=True, related_name="deckhand_watches")
    night_watchkeeper = models.ForeignKey(CrewMember, on_delete=models.PROTECT, null=True, related_name="night_watches")

    def __str__(self):
        return self.date.strftime("%d %b, %Y")

Views:

class CrewMemberViewSet(viewsets.ModelViewSet):
    queryset = CrewMember.objects.all()
    serializer_class = CrewMemberSerializer
    filter_backends = [SearchFilter]
    search_fields = ["department"]

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        instance.active = False
        instance.save()
        return Response(status=status.HTTP_204_NO_CONTENT)

class RosterInstanceViewSet(viewsets.ModelViewSet):
    queryset = RosterInstance.objects.all()
    serializer_class = RosterInstanceSerializer

Serializers:

class CrewMemberSerializer(serializers.ModelSerializer):

    class Meta:
        model = CrewMember
        fields = "__all__"

class RosterInstanceSerializer(serializers.ModelSerializer):

    class Meta:
        model = RosterInstance
        fields = "__all__"

The resulting data looks like this:

    {
        "id": 2,
        "date": "2020-12-09",
        "deckhand_watchkeeper": 1,
        "night_watchkeeper": 3
    }

But I want it to look like this:

    {
        "id": 2,
        "date": "2020-12-09",
        "deckhand_watchkeeper": "Joe Soap",
        "night_watchkeeper": "John Smith"
    }

I can achieve the above output by using StringRelatedField in the RosterInstanceSerializer but then I can no longer add more instances to the RosterInstance model (I believe that is because StringRelatedField is read-only).



Solution 1:[1]

Because StringRelaredField is always read_only, you can use SlugRelatedField instead:

class RosterInstanceSerializer(serializers.ModelSerializer):
    deckhand_watchkeeper = serializers.SlugRelatedField(
        slug_field='deckhand_watchkeeper'
    )
    night_watchkeeper = serializers.SlugRelatedField(
        slug_field='night_watchkeeper'
    )
    class Meta:
        model = RosterInstance
        fields = ['id', 'date', 'deckhand_watchkeeper', 'night_watchkeeper']

Solution 2:[2]

I was created a WritableStringRelatedField to do that.

class WritableStringRelatedField(serializers.SlugRelatedField):
    def __init__(self, display_field=None, *args, **kwargs):
        self.display_field = display_field
        # Set what attribute to be represented.
        # If `None`, use `Model.__str__()` .
        super().__init__(*args, **kwargs)

    def to_representation(self, obj):
        # This function controls how to representation field.
        if self.display_field:
            return getattr(obj, self.display_field)
        return str(obj)

    def slug_representation(self, obj):
        # It will be called by `get_choices()`.
        return getattr(obj, self.slug_field)

    def get_choices(self, cutoff=None):
        queryset = self.get_queryset()
        if queryset is None:
            # Ensure that field.choices returns something sensible
            # even when accessed with a read-only field.
            return {}

        if cutoff is not None:
            queryset = queryset[:cutoff]

        return OrderedDict([
            (
                self.slug_representation(item),
                # Only this line has been overridden,
                # the others are the same as `super().get_choices()`.
                self.display_value(item)
            )
            for item in queryset
        ])

Serializers:

class RosterInstanceSerializer(serializers.ModelSerializer):
    deckhand_watchkeeper = WritableStringRelatedField(
        queryset=CrewMember.objects.all(),
        slug_field='id',
        label='Deckhand Watchkeeper',
    )
    night_watchkeeper = WritableStringRelatedField(
        queryset=CrewMember.objects.all(),
        slug_field='id',
        label='Night Watchkeeper',
    )

    class Meta:
        model = RosterInstance
        fields = "__all__"

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Linh Nguyen
Solution 2