Search code examples
djangorestdjango-modelsdjango-ninja

Why is django-ninja PUT endpoint not using value from request body but instead is returning the default value for field to be updated (ModelSchema)?


Goal

I'm trying to use a Django Ninja API endpoint (with Django Ninja's ModelSchema) to update the time zone preference (tz_preference*) field on my Django app's user model.

*Note: The tz_preference field has a default value and is limited to a list of choices.

Problem

When I test out the API endpoint using /api/docs, the response keeps returning the tz_preference field's default value ("America/Denver") even though I give it other valid values in the request body ("Pacific/Honolulu", "America/Chicago", etc.).

I know that my tz_preference field has a default value of "America/Denver", so this is most likely why the response body is {"tz_preference": "America/Denver"}, but I'm not sure why it's sticking with the default instead of using the value I give it in the request body.

Code

models.py

from django.db import models
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.validators import ASCIIUsernameValidator
from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneField
from zoneinfo import ZoneInfo

TZ_CHOICES = [
    (ZoneInfo('Pacific/Honolulu'), 'Pacific/Honolulu'),
    (ZoneInfo('America/Anchorage'), 'America/Anchorage'),
    (ZoneInfo('America/Los_Angeles'), 'America/Los_Angeles'),
    (ZoneInfo('US/Arizona'), 'US/Arizona'),
    (ZoneInfo('America/Denver'), 'America/Denver'),
    (ZoneInfo('America/Chicago'), 'America/Chicago'),
    (ZoneInfo('America/New_York'), 'America/New_York'),
]

class CustomUser(AbstractBaseUser, PermissionsMixin):
    username_validator = ASCIIUsernameValidator()

    username = models.CharField(
        _("username"),
        max_length=150,
        unique=True,
        db_index=True,
        validators=[username_validator],
        error_messages={
            "unique": _("A user with that username already exists."),
        },
    )
    email = models.EmailField(
        _("email address"),
        unique=True,
        db_index=True,
        help_text=_(
            "Required."
        ),
        error_messages={
            "unique": _("A user with that email address already exists."),
        },
    )
    is_staff = models.BooleanField(_("staff status"), default=False,)
    is_active = models.BooleanField(_("active"), default=True,)

    """ -----------------FIELD I'M TRYING TO CHANGE------------------- """
    tz_preference = TimeZoneField(
        use_pytz=False,
        choices=TZ_CHOICES,
        default="America/Denver",
        choices_display="STANDARD",
    )
    """ -------------------------------------------------------------- """

    class CAT_CHOICES(models.TextChoices):
        S = 'STUDENT', _('Student')
        I = 'INSTRUCTOR', _('Instructor')

    category = models.CharField(max_length=10, choices=CAT_CHOICES.choices)

    objects = TerpUserManager()

    EMAIL_FIELD = "email"
    USERNAME_FIELD = "username"
    REQUIRED_FIELDS = ["email", "category"]

Sorry if I included too much of the code for my custom user model. Since I'm wondering if required fields are causing the unexpected behavior, I left them in.

api.py

# do ye django ninja stuff
from ninja import NinjaAPI

api = NinjaAPI()

# django models stuff (to get my custom user model)
from django.contrib.auth import get_user_model
User = get_user_model()

# ninja schema stuff
from ninja import Schema, ModelSchema

"""------------------------ SCHEMAS ------------------------------"""

class UserTimezoneSchema(ModelSchema):
    class Meta:
        model = User
        fields = ['tz_preference']

class NotFoundSchema(Schema):
    message: str


"""----------------------- ENDPOINTS -----------------------------"""

@api.put("/member/{member_id}", response={200: UserTimezoneSchema, 404: NotFoundSchema})
def change_tz(request, member_id: int, data: UserTimezoneSchema):
    try:
        member = User.objects.get(pk=member_id)
        member.tz_preference = data.tz_preference
        member.save()
    except User.DoesNotExist as e:
        return 404, {'message': 'User does not exist'}

Request and Response info

Request (PUT /api/member/{member_id})

  • path: member_id: 1
  • body: {"tz_preference": "Pacific/Honolulu"}

CURL

curl -X 'PUT' \
  'http://127.0.0.1:8000/api/member/1' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "tz_preference": "Pacific/Honolulu"
}'

Response

  • Code: 200

Body

{
  "tz_preference": "America/Denver"
}

Headers

content-length: 35 
content-type: application/json; charset=utf-8 
cross-origin-opener-policy: same-origin 
date: Mon,01 Jul 2024 18:02:59 GMT 
referrer-policy: same-origin 
server: WSGIServer/0.2 CPython/3.10.14 
x-content-type-options: nosniff 
x-frame-options: DENY 

Responses (bottom section of readout of Django Ninja API auto docs)

screenshot of Responses readout from Django Ninja API documentation

What I think is going wrong:

The image I've included above of the response schemas for this endpoint from the API documentation that Django Ninja automatically generates suggests to me that the default value is automatically set regardless of what the request body is, but I don't have any experience interpreting this automatically generated documentation, so I'm not sure.

And if the problem is due to the default value for this field in my model, I have no idea how to overcome that.

Other possibilities (though these seem less likely to me)

  • Maybe it's because I'm submitting plain strings to the server, but the choices for this field are defined as ZoneInfo objects (I doubt this is the case because plain text data is the only kind of data that can really be submitted through the API, right?)
  • Maybe I should be using a PATCH method instead of PUT
  • Maybe it's not working because my Django project is using csrf protection but I haven't enabled that on the API I construct using Django Ninja
  • Maybe it's because I am trying to only change a single field but there are other required fields that I am not including in the request

Solution

  • Return statements are important

    Turns out I just needed a return statement in the function I wrote to handle the PUT request:

    api.py

    @api.put("/member/{member_id}", response={200: UserTimezoneSchema, 404: NotFoundSchema})
    def change_tz(request, member_id: int, data: UserTimezoneSchema):
        try:
            member = User.objects.get(pk=member_id)
            member.tz_preference = data.tz_preference
            member.save()
            return member
        except User.DoesNotExist as e:
            return 404, {'message': 'User does not exist'}
    

    Thanks to @vitalik, the creator of django-ninja, for pointing this out to me.