Do przesłania pliku używam Django Rest Framework i AngularJs. Mój plik widoku wygląda następująco:
class ProductList(APIView):
authentication_classes = (authentication.TokenAuthentication,)
def get(self,request):
if request.user.is_authenticated():
userCompanyId = request.user.get_profile().companyId
products = Product.objects.filter(company = userCompanyId)
serializer = ProductSerializer(products,many=True)
return Response(serializer.data)
def post(self,request):
serializer = ProductSerializer(data=request.DATA, files=request.FILES)
if serializer.is_valid():
serializer.save()
return Response(data=request.DATA)
Ponieważ ostatnia linia metody postu powinna zwrócić wszystkie dane, mam kilka pytań:
- jak sprawdzić, czy coś jest w środku
request.FILES
? - jak serializować pole pliku?
- jak używać parsera?
Odpowiedzi:
Użyj FileUploadParser , wszystko jest w żądaniu. Zamiast tego użyj metody put, przykład znajdziesz w dokumentacji :)
class FileUploadView(views.APIView): parser_classes = (FileUploadParser,) def put(self, request, filename, format=None): file_obj = request.FILES['file'] # do some stuff with uploaded file return Response(status=204)
źródło
Używam tego samego stosu i szukałem również przykładu przesyłania plików, ale mój przypadek jest prostszy, ponieważ używam ModelViewSet zamiast APIView. Okazało się, że kluczem był hak pre_save. Skończyło się na tym, że użyłem go razem z modułem przesyłania plików kątowych w następujący sposób:
# Django class ExperimentViewSet(ModelViewSet): queryset = Experiment.objects.all() serializer_class = ExperimentSerializer def pre_save(self, obj): obj.samplesheet = self.request.FILES.get('file') class Experiment(Model): notes = TextField(blank=True) samplesheet = FileField(blank=True, default='') user = ForeignKey(User, related_name='experiments') class ExperimentSerializer(ModelSerializer): class Meta: model = Experiment fields = ('id', 'notes', 'samplesheet', 'user') // AngularJS controller('UploadExperimentCtrl', function($scope, $upload) { $scope.submit = function(files, exp) { $upload.upload({ url: '/api/experiments/' + exp.id + '/', method: 'PUT', data: {user: exp.user.id}, file: files[0] }); }; });
źródło
Wreszcie mogę przesłać obraz za pomocą Django. Oto mój działający kod
views.py
class FileUploadView(APIView): parser_classes = (FileUploadParser, ) def post(self, request, format='jpg'): up_file = request.FILES['file'] destination = open('/Users/Username/' + up_file.name, 'wb+') for chunk in up_file.chunks(): destination.write(chunk) destination.close() # File should be closed only after all chuns are added # ... # do some stuff with uploaded file # ... return Response(up_file.name, status.HTTP_201_CREATED)
urls.py
urlpatterns = patterns('', url(r'^imageUpload', views.FileUploadView.as_view())
curl żądanie przesłania
curl -X POST -S -H -u "admin:password" -F "[email protected];type=image/jpg" 127.0.0.1:8000/resourceurl/imageUpload
źródło
with open('/Users/Username/' + up_file.name, 'wb+') as destination:
i całkowicie usunąć zamknięcieModelViewSet
. Poza tym najprawdopodobniej zaimplementowali to lepiej.FileUploadParser
jest to potrzebne, aleMultiPartParser
!Po spędzeniu na tym 1 dnia doszedłem do wniosku, że ...
Dla kogoś, kto musi przesłać plik i wysłać dane, nie ma prostego sposobu, w jaki można go uruchomić. W specyfikacji API JSON występuje otwarty problem . Jedną z możliwości, które widziałem, jest użycie,
multipart/related
jak pokazano tutaj , ale myślę, że bardzo trudno jest go zaimplementować w drf.W końcu zaimplementowałem wysłanie żądania jako
formdata
. Możesz wysłać każdy plik jako plik, a wszystkie inne dane jako tekst. Teraz, jeśli chodzi o wysyłanie danych jako tekst, masz dwie możliwości. przypadek 1) możesz wysłać każde dane jako parę klucz-wartość lub przypadek 2) możesz mieć pojedynczy klucz o nazwie data i wysłać cały plik json jako ciąg znaków w wartości.Pierwsza metoda zadziałałaby po wyjęciu z pudełka, jeśli masz proste pola, ale będzie problemem, jeśli masz zagnieżdżone serializacje. Parser wieloczęściowy nie będzie w stanie przeanalizować zagnieżdżonych pól.
Poniżej przedstawiam implementację dla obu przypadków
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 -> nie są potrzebne żadne specjalne zmiany, nie pokazuję tutaj mojego serializatora jako zbyt długiego z powodu zapisywalnej implementacji pola ManyToMany.
views.py
class PostsViewset(viewsets.ModelViewSet): serializer_class = PostsSerializer #parser_classes = (MultipartJsonParser, parsers.JSONParser) use this if you have simple key value pair as data with no nested serializers #parser_classes = (parsers.MultipartParser, parsers.JSONParser) use this if you want to parse json in the key value pair data sent queryset = Posts.objects.all() lookup_field = 'id'
Teraz, jeśli postępujesz zgodnie z pierwszą metodą i wysyłasz tylko dane inne niż Json jako pary klucz-wartość, nie potrzebujesz niestandardowej klasy parsera. DRF'd MultipartParser wykona zadanie. Ale w drugim przypadku lub jeśli masz zagnieżdżone serializatory (jak pokazałem), będziesz potrzebować niestandardowego parsera, jak pokazano poniżej.
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 = {} # for case1 with nested serializers # parse each field with json for key, value in result.data.items(): if type(value) != str: data[key] = value continue if '{' in value or "[" in value: try: data[key] = json.loads(value) except ValueError: data[key] = value else: data[key] = value # for case 2 # 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)
Ten serializator zasadniczo przeanalizowałby zawartość json w wartościach.
Przykład żądania w post man dla obu przypadków: sprawa 1 ,
Przypadek 2
źródło
Z mojego doświadczenia wynika, że nie musisz robić nic szczególnego z polami pliku, po prostu każ mu korzystać z pola pliku:
from rest_framework import routers, serializers, viewsets class Photo(django.db.models.Model): file = django.db.models.ImageField() def __str__(self): return self.file.name class PhotoSerializer(serializers.ModelSerializer): class Meta: model = models.Photo fields = ('id', 'file') # <-- HERE class PhotoViewSet(viewsets.ModelViewSet): queryset = models.Photo.objects.all() serializer_class = PhotoSerializer router = routers.DefaultRouter() router.register(r'photos', PhotoViewSet) api_urlpatterns = ([ url('', include(router.urls)), ], 'api') urlpatterns += [ url(r'^api/', include(api_urlpatterns)), ]
i jesteś gotowy do przesłania plików:
curl -sS http://example.com/api/photos/ -F 'file=@/path/to/file'
Dodaj
-F field=value
dla każdego dodatkowego pola, które ma twój model. I nie zapomnij dodać uwierzytelniania.źródło
Rozwiązałem ten problem za pomocą ModelViewSet i ModelSerializer. Mam nadzieję, że to pomoże społeczności.
Preferuję też walidację i logowanie do obiektu -> JSON (i odwrotnie) w samym serializatorze, a nie w widokach.
Zrozummy to na przykładzie.
Powiedzmy, chcę utworzyć FileUploader API. Gdzie będzie przechowywać pola takie jak id, ścieżka_pliku, nazwa_pliku, rozmiar, właściciel itp. W bazie danych. Zobacz przykładowy model poniżej:
class FileUploader(models.Model): file = models.FileField() name = models.CharField(max_length=100) #name is filename without extension version = models.IntegerField(default=0) upload_date = models.DateTimeField(auto_now=True, db_index=True) owner = models.ForeignKey('auth.User', related_name='uploaded_files') size = models.IntegerField(default=0)
Teraz w przypadku interfejsów API tego chcę:
Kiedy uruchamiam punkt końcowy GET, chcę mieć wszystkie powyższe pola dla każdego przesłanego pliku.
Ale aby użytkownik utworzył / przesłał plik, dlaczego musi się martwić o przejście wszystkich tych pól. Może po prostu przesłać plik, a następnie, jak przypuszczam, serializator może pobrać pozostałe pola z przesłanego PLIKU.
Searilizer: Pytanie: Stworzyłem poniższy serializator, aby służyć moim celom. Ale nie jestem pewien, czy to właściwy sposób na jego wdrożenie.
class FileUploaderSerializer(serializers.ModelSerializer): # overwrite = serializers.BooleanField() class Meta: model = FileUploader fields = ('file','name','version','upload_date', 'size') read_only_fields = ('name','version','owner','upload_date', 'size') def validate(self, validated_data): validated_data['owner'] = self.context['request'].user validated_data['name'] = os.path.splitext(validated_data['file'].name)[0] validated_data['size'] = validated_data['file'].size #other validation logic return validated_data def create(self, validated_data): return FileUploader.objects.create(**validated_data)
Zestaw widoków w celach informacyjnych:
class FileUploaderViewSet(viewsets.ModelViewSet): serializer_class = FileUploaderSerializer parser_classes = (MultiPartParser, FormParser,) # overriding default query set queryset = LayerFile.objects.all() def get_queryset(self, *args, **kwargs): qs = super(FileUploaderViewSet, self).get_queryset(*args, **kwargs) qs = qs.filter(owner=self.request.user) return qs
źródło
FileUploaderSerializer.validate
metoda?Jeśli ktoś jest zainteresowany najłatwiejszym przykładem z ModelViewset dla Django Rest Framework.
Model jest,
class MyModel(models.Model): name = models.CharField(db_column='name', max_length=200, blank=False, null=False, unique=True) imageUrl = models.FileField(db_column='image_url', blank=True, null=True, upload_to='images/') class Meta: managed = True db_table = 'MyModel'
Serializator,
class MyModelSerializer(serializers.ModelSerializer): class Meta: model = MyModel fields = "__all__"
A widok jest,
class MyModelView(viewsets.ModelViewSet): queryset = MyModel.objects.all() serializer_class = MyModelSerializer
Test w Postman,
źródło
from rest_framework import status from rest_framework.response import Response class FileUpload(APIView): def put(request): try: file = request.FILES['filename'] #now upload to s3 bucket or your media file except Exception as e: print e return Response(status, status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(status, status.HTTP_200_OK)
źródło
def post(self,request): serializer = ProductSerializer(data=request.DATA, files=request.FILES) if serializer.is_valid(): serializer.save() return Response(serializer.data)
źródło
Chciałbym napisać inną opcję, która moim zdaniem jest czystsza i łatwiejsza w utrzymaniu. Będziemy używać defaultRouter do dodawania adresów URL CRUD dla naszego zestawu widoków i dodamy jeszcze jeden stały adres URL określający widok przesyłającego w tym samym zestawie widoków.
**** views.py from rest_framework import viewsets, serializers from rest_framework.decorators import action, parser_classes from rest_framework.parsers import JSONParser, MultiPartParser from rest_framework.response import Response from rest_framework_csv.parsers import CSVParser from posts.models import Post from posts.serializers import PostSerializer class PostsViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer parser_classes = (JSONParser, MultiPartParser, CSVParser) @action(detail=False, methods=['put'], name='Uploader View', parser_classes=[CSVParser],) def uploader(self, request, filename, format=None): # Parsed data will be returned within the request object by accessing 'data' attr _data = request.data return Response(status=204)
Główny adres urls.py projektu
**** urls.py from rest_framework import routers from posts.views import PostsViewSet router = routers.DefaultRouter() router.register(r'posts', PostsViewSet) urlpatterns = [ url(r'^posts/uploader/(?P<filename>[^/]+)$', PostsViewSet.as_view({'put': 'uploader'}), name='posts_uploader') url(r'^', include(router.urls), name='root-api'), url('admin/', admin.site.urls), ]
.- README.
Magia dzieje się, gdy dodamy dekorator @action do naszej metody klasy „uploader”. Określając argument "methods = ['put']", zezwalamy tylko na żądania PUT; idealny do przesyłania plików.
Dodałem również argument "parser_classes", aby pokazać, że możesz wybrać parser, który przeanalizuje twoją zawartość. Dodałem CSVParser z pakietu rest_framework_csv, aby zademonstrować, jak możemy akceptować tylko określone typy plików, jeśli ta funkcjonalność jest wymagana, w moim przypadku akceptuję tylko "Content-Type: text / csv". Uwaga: Jeśli dodajesz niestandardowe parsery, musisz określić je w parsers_classes w zestawie widoku, ponieważ żądanie porówna dozwolony typ_media z parserami głównymi (klasowymi) przed uzyskaniem dostępu do parserów metody przesyłania.
Teraz musimy powiedzieć Django, jak przejść do tej metody i gdzie można ją zaimplementować w naszych adresach URL. Wtedy dodajemy stały adres URL (proste cele). Ten adres URL przyjmie argument „nazwa pliku”, który zostanie później przekazany w metodzie. Musimy przekazać tę metodę „uploader”, określając protokół http („PUT”) na liście do metody PostsViewSet.as_view.
Kiedy wylądujemy w następującym adresie URL
będzie oczekiwać żądania PUT z nagłówkami określającymi „Content-Type” i Content-Disposition: załącznik; filename = "coś.csv".
curl -v -u user:pass http://example.com/posts/uploader/ --upload-file ./something.csv --header "Content-type:text/csv"
źródło
parser_classes
nie ma ograniczenia, które pliki można przesyłać. Pozwala Ci zdecydować, które formaty mogą być używane do składania żądań. Po namyśle, sposób w jaki obsługujesz przesyłanie ... wygląda na to, że umieszczasz dane z CSV w bazie danych. Nie to, o co pytał OP.Jeśli używasz ModelViewSet, właściwie to wszystko! Zajmuje się wszystkim za Ciebie! Wystarczy umieścić pole w swoim ModelSerializer i ustawić
content-type=multipart/form-data;
w swoim kliencie.ALE jak wiesz, nie możesz wysyłać plików w formacie json. (gdy typ zawartości jest ustawiony na application / json w kliencie). Chyba że używasz formatu Base64.
Masz więc dwie możliwości:
ModelViewSet
iModelSerializer
obsłuż zlecenie i wyślij zapytanie za pomocącontent-type=multipart/form-data;
ModelSerializer
asBase64ImageField (or) Base64FileField
i powiedz klientowi, aby zakodował plikBase64
i ustawił rozszerzeniecontent-type=application/json
źródło
models.py
from django.db import models import uuid class File(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) file = models.FileField(blank=False, null=False) def __str__(self): return self.file.name
serializers.py
from rest_framework import serializers from .models import File class FileSerializer(serializers.ModelSerializer): class Meta: model = File fields = "__all__"
views.py
from django.shortcuts import render from rest_framework.parsers import FileUploadParser from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import status from .serializers import FileSerializer class FileUploadView(APIView): permission_classes = [] parser_class = (FileUploadParser,) def post(self, request, *args, **kwargs): file_serializer = FileSerializer(data=request.data) if file_serializer.is_valid(): file_serializer.save() return Response(file_serializer.data, status=status.HTTP_201_CREATED) else: return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
urls.py
from apps.files import views as FileViews urlpatterns = [ path('api/files', FileViews.FileUploadView.as_view()), ]
settings.py
# file uload parameters MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
Wyślij prośbę o wpis na adres
api/files
z dołączonym plikiem doform-data
polafile
. Plik zostanie przesłany do/media
folderu i dodany zostanie rekord bazy danych z identyfikatorem i nazwą pliku.źródło
W django-rest-framework dane żądania są analizowane przez
Parsers
.http://www.django-rest-framework.org/api-guide/parsers/
Domyślnie django-rest-framework przyjmuje klasę parsera
JSONParser
. Sparsuje dane do formatu json. więc pliki nie zostaną z nim przeanalizowane.Jeśli chcemy, aby pliki były parsowane razem z innymi danymi, powinniśmy użyć jednej z poniższych klas parsera.
źródło
application/json
,application/x-www-form-urlencoded
orazmultipart/form-data
.To jedno z podejść, które mam nadzieję, że pomoże.
class Model_File_update(APIView): parser_classes = (MultiPartParser, FormParser) permission_classes = [IsAuthenticated] # it will check if the user is authenticated or not authentication_classes = [JSONWebTokenAuthentication] # it will authenticate the person by JSON web token def put(self, request): id = request.GET.get('id') obj = Model.objects.get(id=id) serializer = Model_Upload_Serializer(obj, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=200) else: return Response(serializer.errors, status=400)
źródło
Możesz uogólnić odpowiedź @ Nithin, aby pracować bezpośrednio z istniejącym systemem serializatorów DRF, generując klasę parsera w celu przeanalizowania określonych pól, które są następnie przekazywane bezpośrednio do standardowych serializatorów DRF:
from django.http import QueryDict import json from rest_framework import parsers def gen_MultipartJsonParser(json_fields): 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 qdict = QueryDict('', mutable=True) for json_field in json_fields: json_data = result.data.get(json_field, None) if not json_data: continue data = json.loads(json_data) if type(data) == list: for d in data: qdict.update({json_field: d}) else: qdict.update({json_field: data}) return parsers.DataAndFiles(qdict, result.files) return MultipartJsonParser
Jest to używane w następujący sposób:
class MyFileViewSet(ModelViewSet): parser_classes = [gen_MultipartJsonParser(['tags', 'permissions'])] # ^^^^^^^^^^^^^^^^^^^ # Fields that need to be further JSON parsed ....
źródło