python - Django/DRF-backend tar inte emot komplett data från POST-förfrågan


Jag försöker skicka data till min Django/DRF-backend via Axios POST-förfrågningar. Vissa av dem har fungerat, men några har inte gjort det. Denna kodsnutt, till exempel, fungerar bra:

      axiosInstance.post(
        `/api/notes/`,
        {
          title,
          content: serialize(editorContent),
        }
      )

Den här, däremot, har orsakat problem:

    axiosInstance.post(
      `/api/notebooks/`,
      {
        name: newNotebookName,
        user,
      }
    )

I det andra utdraget kommer name-attributet igenom till backend, men av någon anledning gör inte user-attributet det. Jag försökte använda console.log för att se om user är undefined vid det ögonblick det skickas till backend och det är det inte.

Jag försökte ändra sättet jag definierar nycklarna i objektet jag skickar till backend så att de inte är dynamiska som i detta svar och resultatet har inte förändrats.

Variabeln user är en sträng som innehåller en e-postadress, som '[email '. Jag trodde kanske att Django blockerade den som standard av säkerhetsskäl, men jag delade strängen vid @ och försökte skicka endast den första delen och det fungerade inte heller.

Jag försökte också definiera en ny variabel så att typen av user är string istället för string | undefined och det fungerar fortfarande inte. Värdet av title är också string och det kommer igenom till backend (det är definierat i validated_data-objektet som min serializer ser i sin create()-metod). Så jag står utan idé om vad detta kan vara. Här är några av mina filer:

axiosAPI.ts
import axios from 'axios'

const baseURL = 'http://localhost:8000/'

export const axiosInstance = axios.create({
  baseURL: baseURL,
  timeout: 5000,
  headers: {
    'xsrfHeaderName': 'X-CSRFTOKEN',
    'xrsfCookieName': 'csrftoken',
    'Authorization': localStorage.getItem('access_token') ? 'JWT ' + localStorage.getItem('access_token') : null,
    'Content-Type': 'application/json',
    'accept': 'application/json',
  }
})

axiosInstance.interceptors.response.use(
  response => response,
  error => {
    const originalRequest = error.config

    // Prevent infinite loops if login credentials invalid
    if (error.response.status === 406 && originalRequest.url === baseURL + 'auth/token/obtain') {
      window.location.href = '/login/'

      return Promise.reject(error);
    }

    // Respond to expired refresh tokens
    if (error.response.data.code === 'token_not_valid' && error.response.status === 401 && error.response.statusText === 'Unauthorized') {
      const refreshToken = localStorage.getItem('refresh_token')
      
      if (refreshToken) {
        const tokenObj = JSON.parse(atob(refreshToken.split('.')[1]))
        const currentTime = Math.ceil(Date.now() / 1000)
        
        if (tokenObj.exp > currentTime) {
          return axiosInstance
            .post('/auth/token/refresh/', { refresh: refreshToken })
            .then(response => {
              localStorage.setItem('access_token', response.data.access)
              localStorage.setItem('refresh_token', response.data.refresh)

              axiosInstance.defaults.headers['Authorization'] = 'JWT ' + response.data.access
              originalRequest.headers['Authorization'] = 'JWT ' + response.data.access

              return axiosInstance(originalRequest);
            })
            .catch(err => {
              console.log(err)
              throw err;
            })
        } else {
          console.log('Refresh token is expired.')
          window.location.href = '/login/'
        }
      } else {
        console.log('Refresh token not available.')
        window.location.href = '/login/'
      }
    }
    
    // Respond to invalid access tokens
    if (error.response.status === 401 && error.response.statusText === 'Unauthorized') {
      const refreshToken = localStorage.getItem('refresh_token')
      
      return axiosInstance
        .post('/auth/token/refresh/', { refresh: refreshToken })
        .then(response => {
          localStorage.setItem('access_token', response.data.access)
          localStorage.setItem('refresh_token', response.data.refresh)
          
          axiosInstance.defaults.headers['Authorization'] = 'JWT ' + response.data.access
          originalRequest.headers['Authorization'] = 'JWT ' + response.data.access

          return axiosInstance(originalRequest)
        }).catch(err => {
          console.log(err)
          throw err;
        })
    }
  }
)
notebooks/serializers.py
import bleach
import json

from rest_framework import serializers

from authentication.models import ErsatzNoteUser
from .models import Note, Notebook

class NoteSerializer(serializers.ModelSerializer):
  note_id = serializers.SlugField(source='id', read_only=True, required=False)
  title = serializers.JSONField(required=False)
  content = serializers.CharField(required=False)
  notebook = serializers.PrimaryKeyRelatedField(read_only=True, required=False)

  date_modified = serializers.DateField(read_only=True, required=False)
  date_created = serializers.DateField(read_only=True, required=False)

  def create(self, validated_data):
    title = json.dumps(validated_data['title'])

    # Workaround to fix a currently unpatched bug in Slate
    # that occurs when an editor's contents begin with a list
    content = validated_data['content']
    if content.startswith('<ul') or content.startswith('<ol'):
      content = '<p></p>' + content

    response_data = {
      'title': title,
      'content': content,
    }

    return Note.objects.create(**response_data)

  def update(self, instance, validated_data):
    instance.title = json.dumps(validated_data['title'])

    # See the above comment in the 'create' method
    content = validated_data['content']
    if content.startswith('<ul') or content.startswith('<ol'):
      content = '<p></p>' + content
    
    instance.content = content

    instance.save()

    return instance

  class Meta:
    model = Note
    fields = [ 'note_id', 'title', 'content', 'notebook', 'date_modified', 'date_created' ]

class NotebookSerializer(serializers.ModelSerializer):
  notebook_id = serializers.SlugField(source='id', read_only=True, required=False)
  name = serializers.CharField(max_length=64, default='')
  notes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

  def create(self, validated_data):
    # notebook_data = {
    #   'name': validated_data['name'],
    #   'user': ErsatzNoteUser.objects.get(email=validated_data['user']),
    # }
    print(validated_data)
    
    return Notebook.objects.create(name=validated_data['name'], user=ErsatzNoteUser.objects.get(email=validated_data['user']))
  
  class Meta:
    model = Notebook
    fields = [ 'notebook_id', 'name', 'notes', 'date_modified', 'date_created' ]
notebooks/views.py
import json

from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response

from .serializers import NoteSerializer, NotebookSerializer
from .models import Note, Notebook

class NoteViewSet(viewsets.ModelViewSet):
  serializer_class = NoteSerializer
  
  def get_queryset(self):
    return self.request.user.notes.all()
  
  def retrieve(self, request, *args, **kwargs):
    instance = self.get_object()
    serializer = NoteSerializer(instance)

    title = json.loads(serializer.data['title'])

    response_data = {
      'note_id': serializer.data['note_id'],
      'title': title,
      'content': serializer.data['content'],
      'notebook': serializer.data['notebook'],
      'date_modified': serializer.data['date_modified'],
      'date_created': serializer.data['date_created'],
    }

    return Response(response_data)

class NotebookViewSet(viewsets.ModelViewSet):
  serializer_class = NotebookSerializer

  def get_queryset(self):
    return self.request.user.notebooks.all()
notebooks/models.py
from django.db import models
from django.contrib.auth.models import User
from jsonfield import JSONField
from django.conf import settings

from .helpers import generate_slug

class Note(models.Model):
  """ Represents an individual note. """

  id = models.SlugField(max_length=settings.MAX_SLUG_LENGTH, primary_key=True)
  title = JSONField(null=True)
  content = models.TextField(null=True)
  notebook = models.ForeignKey('Notebook', related_name='notes', on_delete=models.CASCADE, null=True, blank=True)
  user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='notes', on_delete=models.CASCADE, null=True, blank=True)
  date_created = models.DateField(auto_now_add=True)
  date_modified = models.DateField(auto_now=True)
  
  def save(self, *args, **kwargs):
    if not self.id:
      self.id = generate_slug(self, settings.MAX_SLUG_LENGTH)
    # Temporary expedients for the sake of development
    if not self.notebook:
      self.notebook = Notebook.objects.get(id='YyOzNhMFMPtN8HM')
    super(Note, self).save(*args, **kwargs)
  
  def __str__(self):
    return self.id
  
  class Meta:
    ordering = [ '-date_modified', '-date_created', ]

class Notebook(models.Model):
  """ A collection of individual notes. """
  
  id = models.SlugField(max_length=settings.MAX_SLUG_LENGTH, primary_key=True)
  name = models.CharField(max_length=64, default='')
  user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='notebooks', on_delete=models.CASCADE)
  date_created = models.DateField(auto_now_add=True)
  date_modified = models.DateField(auto_now=True)

  def save(self, *args, **kwargs):
    if not self.id:
      self.id = generate_slug(self, settings.MAX_SLUG_LENGTH)
    super(Notebook, self).save(*args, **kwargs)

  def __str__(self):
    return self.name
  
  class Meta:
    ordering = [ '-date_modified', '-date_created', 'name' ]

Tveka inte att fråga om du vill ha mer information.

REDIGERA: Ändrade NotebookViewSet så att det har en retrieve()-metod, resultatet har inte förändrats:

class NotebookViewSet(viewsets.ModelViewSet):
  serializer_class = NotebookSerializer

  def get_queryset(self):
    return self.request.user.notebooks.all()
  
  def retrieve(self, request, *args, **kwargs):
    instance = self.get_object()
    serializer = NotebookSerializer(instance)

    response_data = {
      'name': serializer.data['name'],
      'user': serializer.data['user'],
    }

    return Response(response_data)

Svar #1

I ditt exempel har din NotebookViewSet-klass ingen retrieve-metod.

Svar #2

Det visar sig att under huven betraktar DRF ForeignKey-fält som read_only. Det betyder att det enbart ska användas för att representera objektet och inte bör vara en del av någon skapa- eller uppdateringsprocess. Enligt dokumentationen:

Skrivskyddade fält ingår i API-utdata, men bör inte ingå i indatan vid skapande eller uppdatering. Eventuella 'read_only'-fält som felaktigt inkluderas i serialiserarens indata kommer att ignoreras.

Källa.

Men detta spelade ingen roll för min databas, som kräver att jag identifierar användaren som motsvarar varje ny Notebook vid tidpunkten för dess skapande, annars bryts en NOT NULL-begränsning. Så jag skrev helt enkelt över standardmetoden create() i ModelViewSet för att manuellt skicka vidare user-strängen till min NotebookSerializer som en del av context, vilket tillåter mig att använda den för att få motsvarande användarobjekt i databasen och tilldela det till den nyligen skapade Notebook. Jag gjorde på detta sätt:

class NotebookViewSet(viewsets.ModelViewSet):
  serializer_class = NotebookSerializer

  def get_queryset(self):
    return self.request.user.notebooks.all()

  def perform_create(self, serializer):
    serializer.save()

  def create(self, request, *args, **kwargs):
    serializer = NotebookSerializer(data=request.data, context={ 'request': request })
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)

    return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class NotebookSerializer(serializers.ModelSerializer):
  notebook_id = serializers.SlugField(source='id', read_only=True, required=False)
  name = serializers.CharField(max_length=64, default='')
  notes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

  def create(self, validated_data):
    return Notebook.objects.create(
      user=ErsatzNoteUser.objects.get(email=self.context['request'].user),
      **validated_data
    )
  
  class Meta:
    model = Notebook
    fields = [ 'notebook_id', 'name', 'notes', 'date_modified', 'date_created' ]

Det var allt som krävdes.