#javascript #python #django #api #django-rest-framework
#javascript #python #django #API #django-rest-framework
Вопрос:
===========JS==========
const data = {
"id": "d33e6dca-9152-4ded-96e3-51b2f24423d8",
"logo": null,
"name": "Company 1",
"account": {
"id": "d33e6dca-9152-4ded-96e3-51b2f24423d9",
"name": "Account Name",
"company": "d33e6dca-9152-4ded-96e3-51b2f24423d8"
},
"modules": [
{
"id": "d33e6dca-9152-4ded-96e3-51b2f24423d1",
"name": "Module 1"
},
{
"id": "d33e6dca-9152-4ded-96e3-51b2f24423d2",
"name": "Module 2"
},
],
"addresses": [
{
"id": "d33e6dca-9152-4ded-96e3-51b2f24423d4",
"name": "Address 1",
"company": "d33e6dca-9152-4ded-96e3-51b2f24423d8"
},
{
"id": "d33e6dca-9152-4ded-96e3-51b2f24423d5",
"name": "Address 2",
"company": "d33e6dca-9152-4ded-96e3-51b2f24423d8"
},
],
}
let formData = new FormData();
Object.keys(data).map(key=>{
formData.append(key, typeof data[key] === "object" amp;amp; key!=="logo"? JSON.stringify(data[key]):data[key])
})
const config = {
headers: {'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW'}
};
//axios.put(url, data).then(res => {console.log(".....Response!!! UPDATE")}).catch(err => {console.log(err.response.data)})
axios.put(url, formData, config).then(res => {console.log(".....Response!!! UPDATE")}).catch(err => {console.log(err.response.data)})
===========Models==========
class Address(BaseModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(_('name'), max_length=254, unique=True)
company = models.ForeignKey("Company", on_delete=models.CASCADE, related_name="our_addresses")
class Module(BaseModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=32)
class Account(CompanyBaseModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=32)
company = models.OneToOneField("Company", on_delete=models.CASCADE, related_name="account")
class Company(BaseModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(_('name'), max_length=254, unique=True)
logo = models.ImageField(_('Company Logo'), upload_to=logo_path, storage=PublicMediaStorage(),
blank=True, null=True, default=None)
addresses = models.ManyToManyField(Address, related_name="companies")
modules = models.ManyToManyField(Module)
===========Serializers==========
class AddressFormSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(read_only=False, required=False, allow_null=True)
class Meta:
model = Address
fields = '__all__'
read_only_fields = ['company', ]
class ModuleFormSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(read_only=False) # required ONLY for M2M Update cos validation will fail without it
class Meta:
model = Module
fields = "__all__"
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
fields = "__all__"
read_only_fields = ['id', 'company']
class CompanyFormSerializer(serializers.ModelSerializer):
logo = serializers.ImageField(max_length=None, use_url=True, allow_null=True)
account = AccountSerializer()
modules = ModuleFormSerializer(many=True)
addresses = AddressFormSerializer(many=True)
class Meta:
model = Company
fields = "__all__"
def create(self, validated_data):
with transaction.atomic():
request = self.context['request']
account_data = validated_data.pop('account')
modules_data = validated_data.pop('modules')
address_data = validated_data.pop('addresses')
company = Company.objects.create(**validated_data)
Billing.objects.create(company=company, **account_data)
addresses = [Address.objects.create(company=company, **a) for a in address_data]
company.addresses.add(*addresses)
modules = Module.objects.filter(id__in=[m.get('id') for m in modules_data])
company.modules.add(*modules)
return company
def update(self, instance, validated_data):
with transaction.atomic():
#... lets me not bore you with the logic here
return instance
===========ModelViewSet==========
class CompanyViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated, )
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=self.request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
serializer.save()
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
log.warning(type(request.data))
#json.loads(request.data['account']) is serialized as String if 'multipart/form-data'
# request.data._mutable = True
# request.data['account'] = json.loads(request.data['account'])
# request.data['addresses'] = json.loads(request.data['addresses'])
# request.data['modules'] = json.loads(request.data['modules'])
# request.data._mutable = False
serializer = self.get_serializer(instance, data=request.data, partial=partial, context={'request': request})
serializer.is_valid(raise_exception=True) # It fails here....
company = serializer.save()
serializer = CompanyFormSerializer(company)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get_serializer_class(self):
if self.action in ['create', 'update', 'retrieve', 'partial_update']:
return CompanyFormSerializer
else:
return CompanySerializer
===========Settings Files==========
REST_FRAMEWORK = {
......,
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser',
),
}
Если мы сосредоточимся на обновлении (хотя оно одинаково как для POST, так и для PUT), я ожидал, что serializer.is_valid(raise_exception= True) будет проверять, но он не проверяет, вместо этого он продолжает жаловаться, что требуется учетная запись, модули, адреса
Обратите внимание, что все работает хорошо, когда я отправляю ИЛИ ПОМЕЩАЮ с помощью axios.put(url, data), поэтому проблема возникает, когда я пытаюсь использовать multipart / form-data, передающий FormData вместо чистого JSON
Комментарии:
1. Итак, ваши вложенные объекты представляют собой строки json, а остальные — данные формы? Это объясняет это.
2. @Melvyn это правильно, я вижу, что при проверке
serializer.initial_data
, однако, это не мое ожидаемое поведение. Обратите внимание на строки с комментариями внутриCompanyViewSet
, где я пытался это исправить, делая что-то вродеrequest.data['account'] = json.loads(request.data['account'])
, но это не могло это исправить.3. И почему вы выполняете эту дополнительную работу вместо того, чтобы просто публиковать все как данные формы ?
4. @Melvyn кто хотел бы выполнять дополнительную работу обычно? Посмотрите на это изображение и обратите внимание, что на самом деле это был запрос, как предложено в Документе, см. Изображение ibb.co/M8LzsM5 из переменных и обратите внимание, что все следующие объекты, такие как
billing
,modules
etc, вошли в виде строки. Это было то, что я пытался исправить в прокомментированном коде. Я буду признателен, если вы укажете мне на детали, которые я делаю неправильно.5. Хорошо, я понимаю, что вы пытаетесь. Я полностью пропустил json.loads(), потому что мои глаза были полузакрыты. Я бы, вероятно, обработал файл вне сериализатора и просто позволил сериализатору проверить другие данные. Но у вас может не быть такой свободы в наборе представлений с одним сериализатором для каждого просмотра.
Ответ №1:
Сериализатору просто нужен словарь данных. Не обязательно должен быть request.data. Таким образом, вы можете просто создать хороший диктант самостоятельно из того, что анализатор сделал с вашим запросом:
data = {
'account': json.loads(self.request.data.get('account'))
# etc..
}
data['logo'] = request.FILES.get('logo')
serializer = self.get_serializer(instance, data=data, ...)
Во всяком случае, теперь он должен завершиться неудачей в другом месте.
Длинный ответ
Итак, я поиграл с этим, и ниже приведены просмотр и тест, которые я использовал, чтобы убедиться, что метод работает. Единственная ошибка заключается в том, что я не предоставляю действительное изображение, но остальные поля принимаются.
Я подключил его /main/api/companies/
, как вы можете видеть из тестового примера.
# view.py
import json
from rest_framework import generics, status
from rest_framework.response import Response
from . import serializers, models
class CompanyUpdate(generics.RetrieveUpdateAPIView):
serializer_class = serializers.CompanyFormSerializer
queryset = models.Company.objects.all()
json_fields = ("account", "modules", "addresses")
normal_fields = ("id", "name", "logo")
def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
instance = self.get_object()
data = dict(
((key, json.loads(self.request.data.get(key))) for key in self.json_fields)
)
data.update(((key, request.data.get(key)) for key in self.normal_fields))
serializer: serializers.CompanyFormSerializer = self.get_serializer(
instance=instance, data=data, context={"request": request}
)
serializer.is_valid(raise_exception=True)
return Response(status=status.HTTP_200_OK) # NOT_REACHED
Тестовый пример:
from io import BytesIO
import json
import typing as t
import uuid
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from . import models
class UploadTest(TestCase):
client_class = APIClient
base_url = "/main/api/companies/"
@classmethod
def setUpTestData(cls):
company = models.Company.objects.create(name="honest mike",)
account = models.Account.objects.create(name="account name", company=company)
gardening = models.Module.objects.create(name="gardening")
kitchen = models.Module.objects.create(name="kitchen")
first_address = models.Address.objects.create(name="address 1", company=company)
second_address = models.Address.objects.create(
name="address 2", company=company
)
img = BytesIO(b"x89PNGrbogus")
img.name = "fake.png"
cls.original = {
"id": str(company.id),
"logo": img,
"name": company.name.title(),
"account": {
"id": str(account.id),
"name": account.name.title(),
"company": str(company.id),
},
"modules": [
{"id": str(gardening.id), "name": gardening.name.title()},
{"id": str(kitchen.id), "name": kitchen.name.title()},
],
"addresses": [
{
"id": str(first_address.id),
"name": "Main Street 1",
"company": str(company.id),
},
{
"id": str(second_address.id),
"name": "Church Avenue 2",
"company": company.id,
},
],
}
cls.company = company
cls.account = account
cls.gardening = gardening
cls.kitchen = kitchen
cls.first_address = first_address
cls.second_address = second_address
cls.json_fields = ("account", "modules", "addresses")
def test_url(self):
url = f"{self.base_url}{str(self.company.id)}/"
response = self.client.get(url)
self.assertEqual(str(self.company.id), response.data.get("id"))
data = self.jsonify()
response = self.client.put(url, data=data, format="multipart")
print(response.data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue('modules' not in response.data)
self.assertEqual(len(response.data.keys()), 1)
self.assertTrue('logo' in response.data)
def jsonify(self):
class Encoder(json.JSONEncoder):
def default(self, o: t.Any) -> t.Any:
if isinstance(o, uuid.UUID):
return str(o)
return super().default(o)
data = self.original
for field in self.json_fields:
data[field] = json.dumps(self.original[field], cls=Encoder)
return data