web-dev-qa-db-fra.com

En utilisant Django Rest Framework, comment puis-je télécharger un fichier ET envoyer une charge utile JSON?

J'essaie d'écrire un Django Rest Framework API API qui peut recevoir un fichier ainsi qu'une charge utile JSON. J'ai défini MultiPartParser comme analyseur de gestionnaire.

Cependant, il semble que je ne puisse pas faire les deux. Si j'envoie la charge utile avec le fichier en tant que demande en plusieurs parties, la charge utile JSON est disponible de manière modifiée dans le request.data (première partie de texte jusqu'au premier deux-points comme clé, le reste sont les données). Je peux très bien envoyer les paramètres sous forme standard - mais le reste de mon API accepte les charges utiles JSON et je voulais être cohérent. Le request.body ne peut pas être lu car il soulève *** RawPostDataException: You cannot access body after reading from request's data stream

Par exemple, un fichier et cette charge utile dans le corps de la demande:
{"title":"Document Title", "description":"Doc Description"}
Devient:
<QueryDict: {u'fileUpload': [<InMemoryUploadedFile: 20150504_115355.jpg (image/jpeg)>, <InMemoryUploadedFile: Front end lead.doc (application/msword)>], u'{%22title%22': [u'"Document Title", "description":"Doc Description"}']}>

Y a-t-il un moyen de faire cela? Puis-je manger mon gâteau, le garder et ne pas prendre de poids?

Edit: Il a été suggéré que cela pourrait être une copie de Django REST Framework upload image: "Les données soumises n'étaient pas un fichier" . Ce n'est pas le cas. Le téléchargement et la demande se fait en plusieurs parties, et gardez à l'esprit que le fichier et le téléchargement sont corrects. Je peux même compléter la demande avec des variables de formulaire standard. Mais je veux voir si je peux y ajouter une charge utile JSON.

25
Harel

Pour quelqu'un qui a besoin de télécharger un fichier et d'envoyer des données, il n'y a aucun moyen direct de le faire fonctionner. Il y a un problème ouvert dans les spécifications de l'api json pour cela. Une possibilité que j'ai vue est d'utiliser multipart/related comme indiqué ici , mais je pense qu'il est très difficile de l'implémenter dans drf.

Enfin, ce que j'avais implémenté, c'était d'envoyer la demande sous la forme formdata. Vous enverriez chaque fichier en tant que fichier et toutes les autres données sous forme de texte. Maintenant, pour envoyer les données sous forme de texte, vous pouvez avoir une seule clé appelée data et envoyer le json entier sous forme de chaîne en valeur.

Models.py

class Posts(models.Model):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    caption = models.TextField(max_length=1000)
    media = models.ImageField(blank=True, default="", upload_to="posts/")
    tags = models.ManyToManyField('Tags', related_name='posts')

serializers.py -> aucun changement spécial nécessaire, ne montrant pas mon sérialiseur ici comme trop long en raison de l'implémentation du champ ManyToMany accessible en écriture.

views.py

class PostsViewset(viewsets.ModelViewSet):
    serializer_class = PostsSerializer
    parser_classes = (MultipartJsonParser, parsers.JSONParser)
    queryset = Posts.objects.all()
    lookup_field = 'id'

Vous aurez besoin d'un analyseur personnalisé comme indiqué ci-dessous pour analyser json.

utils.py

from Django.http import QueryDict
import json
from rest_framework import parsers

class MultipartJsonParser(parsers.MultiPartParser):

    def parse(self, stream, media_type=None, parser_context=None):
        result = super().parse(
            stream,
            media_type=media_type,
            parser_context=parser_context
        )
        data = {}
        # find the data field and parse it
        data = json.loads(result.data["data"])
        qdict = QueryDict('', mutable=True)
        qdict.update(data)
        return parsers.DataAndFiles(qdict, result.files)

L'exemple de demande dans postman case2

ÉDITER:

voir this réponse étendue si vous souhaitez envoyer chaque donnée sous forme de paire valeur/clé

6
nithin

Je sais que c'est un vieux fil, mais je viens de le découvrir. J'ai dû utiliser MultiPartParser pour que mon fichier et les données supplémentaires soient réunis. Voici à quoi ressemble mon code:

# views.py
class FileUploadView(views.APIView):
    parser_classes = (MultiPartParser,)

    def put(self, request, filename, format=None):
        file_obj = request.data['file']
        ftype    = request.data['ftype']
        caption  = request.data['caption']
        # ...
        # do some stuff with uploaded file
        # ...
        return Response(status=204)

Mon code AngularJS utilisant ng-file-upload est:

file.upload = Upload.upload({
  url: "/api/picture/upload/" + file.name,
  data: {
    file: file,
    ftype: 'final',
    caption: 'This is an image caption'
  }
});
4
themanatuf

J'envoie du JSON et une image pour créer/mettre à jour un objet produit. Ci-dessous est une création APIView qui fonctionne pour moi.

Sérialiseur

class ProductCreateSerializer(serializers.ModelSerializer):
    class Meta:
         model = Product
        fields = [
            "id",
            "product_name",
            "product_description",
            "product_price",
          ]
    def create(self,validated_data):
         return Product.objects.create(**validated_data)

Vue

from rest_framework  import generics,status
from rest_framework.parsers import FormParser,MultiPartParser

class ProductCreateAPIView(generics.CreateAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductCreateSerializer
    permission_classes = [IsAdminOrIsSelf,]
    parser_classes = (MultiPartParser,FormParser,)

    def perform_create(self,serializer,format=None):
        owner = self.request.user
        if self.request.data.get('image') is not None:
            product_image = self.request.data.get('image')
            serializer.save(owner=owner,product_image=product_image)
        else:
            serializer.save(owner=owner)

Exemple de test:

def test_product_creation_with_image(self):
    url = reverse('products_create_api')
    self.client.login(username='testaccount',password='testaccount')
    data = {
        "product_name" : "Potatoes",
        "product_description" : "Amazing Potatoes",
        "image" : open("local-filename.jpg","rb")
    }
    response = self.client.post(url,data)
    self.assertEqual(response.status_code,status.HTTP_201_CREATED)
3
sarc360

Il est très simple d'utiliser un article en plusieurs parties et une vue régulière, si c'est une option.

Vous envoyez le json en tant que champ et les fichiers en tant que fichiers, puis traitez dans une seule vue.

Voici un simple client python et un serveur Django:

Client - envoi de plusieurs fichiers et d'un objet codé json arbitraire:

import json
import requests

payload = {
    "field1": 1,
    "manifest": "special cakes",
    "nested": {"arbitrary":1, "object":[1,2,3]},
    "hello": "Word" }

filenames = ["file1","file2"]
request_files = {}
url="example.com/upload"

for filename in filenames:
    request_files[filename] = open(filename, 'rb')

r = requests.post(url, data={'json':json.dumps(payload)}, files=request_files)

Serveur - consommer le json et enregistrer les fichiers:

@csrf_exempt
def upload(request):
    if request.method == 'POST':
        data = json.loads(request.POST['json']) 
        try:
            manifest = data['manifest']
            #process the json data

        except KeyError:
            HttpResponseServerError("Malformed data!")

        dir = os.path.join(settings.MEDIA_ROOT, "uploads")
        os.makedirs(dir, exist_ok=True)

        for file in request.FILES:
            path = os.path.join(dir,file)
            if not os.path.exists(path):
                save_uploaded_file(path, request.FILES[file])           

    else:
        return HttpResponseNotFound()

    return HttpResponse("Got json data")


def save_uploaded_file(path,f):
    with open(path, 'wb+') as destination:
        for chunk in f.chunks():
            destination.write(chunk)
0
user1656671