1

My Beta model's stage field provides 5 choices. I want my serializer to not always accept all these choices but only some of them according to the serialized object's actual stage value. For example, if my_beta_object.stage == 1, then the serializer should expect (and offer) only stages 2 and 3, if my_beta_object.stage == 2, only stages 2 and 4, etc.

# models.py
class Beta(models.Model):
    class BetaStage(models.IntegerChoices):
        REQUESTED = (1, "has been requested")
        ACCEPTED = (2, "has been accepted")
        REFUSED = (3, "has been refused")
        CORRECTED = (4, "has been corrected")
        COMPLETED = (5, "has been completed")

    stage = models.ChoiceField(choices=self.BetaStage.choices)

# serializers.py
class BetaActionSerializer(serializers.ModelSerializer):
    stage = serializers.ChoiceField(
        # choices=?
    )

    class Meta:
        model = Beta
        fields = ("stage",)

# views.py
class BetaViewSet(viewsets.ModelViewSet):
    serializer_class = BetaSerializer

    def get_serializer_class(self):
        if self.action == "update":
            return BetaActionSerializer
        return self.serializer_class

How can I dynamically limit the choices of that field according to the serialized object's field value?

  • 1
    can you share your views file? – Sırrı Kırımlıoğlu May 14 '21 at 22:25
  • I've added it to the question. Note that like the model and serializer it's abridged. Also BetaSerializer is not included but is used for other actions. – Paulo Modulo May 14 '21 at 22:37
  • 1
    Is the serializer used widely? if not, then keep validation in the view in my opinion. BUT if it is used everywhere, then you can use a method called `validate`. Additionally, you can create custom validators. Ref the DRF docs for validators; then, add it to your field. I think the `validate` method will be better because you can access all fields. – acw May 15 '21 at 02:22
  • Validation is only half of the problem, though. I would like to have the form's stage field offer a limited set of choices to select from dependent on the serialized object's stage value. – Paulo Modulo May 15 '21 at 11:25

2 Answers2

1

You could try to override the __init__ method of your serializer and then dynamically generates the choices. However, it's not as simple as choices=my_generated_choices. It's a bit more complicated, and there's an in-depth solution on that topic over there

The alternative and simpler method is to use the validate() method, which is triggered at the end of the validation process, after each field has been validated successfully. You could do the following:

  • Write a static dict that maps each status to its valid status choices
  • In the validate method, if updating, check if the new status is a valid choice based on your current status, using your static dict
  • If not, raise a ValidationError
# Example of mapping. Might want to make it pretties and put it in the model itself
mapping = {
  1: [2, 3],
  2: [1, 3],
  # ...
}

# And in validate
def validate(self, data):
    # We're in an update scenario
    if self.instance not None: 
        # It doesnt appear to be required, so use .get()
        new_stage = data.get("stage")
        # Not sure if an instance can have no stage?
        if new_stage is not None and self.instance.stage is not None: 
            # Our check
            if new_stage not in mapping[self.instance.stage]:
                raise ValidationError("Invalid stage")
Jordan Kowal
  • 1,444
  • 8
  • 18
  • The thread you shared was very interesting. While it does allow setting choices dynamically by overriding ChoiceField's `__init__`, it can't do it according to the serialized object's field value, because it doesn't have access to that value at `__init__` (or at least not in my little understanding of the source code). – Paulo Modulo May 15 '21 at 23:51
0

Part of the explanation and a start for a solution I found here. By design, I cannot get the context at fields' initialization apparently. I need to redefine the ChoiceField choices attribute at serializer's initialization.

# serializers.py
class BetaActionSerializer(ModelSerializer):
    def __init__(self, *args, **kwargs):
        super(ModelSerializer, self).__init__(*args, **kwargs)
        if self.instance:
            if self.instance.stage == Beta.BetaStage.REQUESTED.value:
                self.fields["stage"].choices = [
                    (Beta.BetaStage.ACCEPTED, Beta.BetaStage.ACCEPTED.name),
                    (Beta.BetaStage.REFUSED, Beta.BetaStage..REFUSED.name),
                ]
            etc.

I'm not marking this answer as a solution because I'm the original poster and I'm not sure this is a solid or desirable solution. However I'm hopeful this could help someone with the same or a similar issue in the future, as I've struggled finding information myself.