Let's say I have a django model with an IntegerField
with a defined set of choices and then a django ninja schema and endpoint to update this model. How can I access the display text for the IntegerField
(i.e., get_foo_display
)?
In other words, my current schema returns a 1, 2, or 3 for the "rating" field. How can I get it to return the display text as well?
from django.db import models
from django.utils.translation import gettext_lazy as _
class PracticeSession(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=False,related_name="practice_sessions", db_index=True,
)
class RATING_CHOICES(models.IntegerChoices):
UNHELP = 1, _('unhelpful')
NEITHER = 2, _('neither helpful nor unhelpful')
HELP = 3, _('helpful')
rating = models.IntegerField(choices=RATING_CHOICES.choices)
is_practice_done = models.BooleanField(default=False)
from ninja import NinjaAPI, Schema, ModelSchema
from ninja.security import django_auth
from practice.models import PracticeSession
api = NinjaAPI(csrf=True, auth=django_auth)
class UpdatePracticeSessionSchema(ModelSchema):
class Meta:
model = PracticeSession
fields = ['is_practice_done', 'rating']
@api.put(
"member/{member_id}/practice_session/{sesh_id}/update/",
response={200: UpdatePracticeSessionSchema, 404: NotFoundSchema}
)
def update_practice_sesh(request, member_id: int, sesh_id: int, data: UpdatePracticeSessionSchema):
try:
practice_sesh = PracticeSession.objects.get(pk=sesh_id)
practice_sesh.is_practice_done = data.is_practice_done
practice_sesh.rating = data.rating
practice_sesh.save()
return practice_sesh
except Exception as e:
print(f"Error in update_practice_sesh: {str(e)}")
return 404, {'message': f'Error: {str(e)}'}
I tried adding rating_choices = PracticeSession.rating.field.choices
to my UpdatePracticeSessionSchema
before class: Meta
, but this triggered a pydantic error (see below), and, besides, this extra field in my Schema would have only gotten me a step closer to creating some sort of a mapping object (which would involve me writing extra javascript to extract the display text for whatever integer value the schema returns), but I'd much rather just have my Schema return the display text for the exact integer value it's returning as well.
pydantic.errors.PydanticUserError: A non-annotated attribute was detected: rating_choices = [(1, 'unhelpful'), (2, 'neither helpful nor unhelpful'), (3, 'helpful')]
. All model fields require a type annotation; if rating_choices
is not meant to be a field, you may be able to resolve this error by annotating it as a ClassVar
or updating model_config['ignored_types']
.
Since you want to override the behavior of how the field is serialized, you will want to use custom serialization logic. Strangely enough, this should be done using field validators instead of field serializers. More on why in this related StackOverflow question.
First define a simple Schema
for your rating field:
from ninja import Schema
class RatingSchema(Schema):
# You can choose the actual names you want for these fields
value: id
label: str
And then update your model schema to use this field schema and "validate" it with field_validator
:
from ninja import ModelSchema
from pydantic import field_validator
from practice.models import PracticeSession
class UpdatePracticeSessionSchema(ModelSchema):
# rating should use your custom schema
rating: RatingSchema
class Meta:
model = PracticeSession
fields = ['is_practice_done', 'rating']
# mode='before' ensures that this runs before other validators
# Note that the value is an int since this is what the PracticeSession.rating returns
@field_validator('rating', mode='before')
@classmethod
def validate_rating(cls, value: int) -> RatingSchema:
# Turn the value from the model into a RATING_CHOICES
# This will throw a ValueError if it's an invalid value
# which works in our favor since Pydantic will catch this
# and throw a ValidationError
rating = PracticeSession.RATING_CHOICES(value)
return RatingSchema(value=rating.value, label=rating.label)