vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
7.25k stars 430 forks source link

Unable to upload file (always get 'field required' message) #201

Closed aprilahijriyan closed 3 years ago

aprilahijriyan commented 3 years ago

Hi @vitalik, I'm trying to upload a file but it's always getting a "field required" message.

Screenshot from 2021-08-15 08-55-14

Handler:

FileParam = File
def file_create(request: HttpRequest,
        name: str = FileParam(..., max_length=100),
        description: Optional[str] = FileParam(None, max_length=500),
        file: UploadedFile = FileParam(...),
        folder: Optional[UUID] = FileParam(None)
    ):

Has anything been missed?

stephenrauch commented 3 years ago

You are using File (eg: FileParam) for other than files:

https://django-ninja.rest-framework.com/tutorial/file-params/

aprilahijriyan commented 3 years ago

So, how do I get the name and the folder together when the file is uploaded?

aprilahijriyan commented 3 years ago

I've tried to combine the Form class, but get an error as follows.

Traceback (most recent call last):
  File "/home/titiw/.cache/pypoetry/virtualenvs/simpanfile-lylPHfKd-py3.8/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/home/titiw/.cache/pypoetry/virtualenvs/simpanfile-lylPHfKd-py3.8/lib/python3.8/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/titiw/.cache/pypoetry/virtualenvs/simpanfile-lylPHfKd-py3.8/lib/python3.8/site-packages/ninja/openapi/views.py", line 25, in openapi_json
    schema = api.get_openapi_schema()
  File "/home/titiw/.cache/pypoetry/virtualenvs/simpanfile-lylPHfKd-py3.8/lib/python3.8/site-packages/ninja/main.py", line 349, in get_openapi_schema
    return get_schema(api=self, path_prefix=path_prefix)
  File "/home/titiw/.cache/pypoetry/virtualenvs/simpanfile-lylPHfKd-py3.8/lib/python3.8/site-packages/ninja/openapi/schema.py", line 22, in get_schema
    openapi = OpenAPISchema(api, path_prefix)
  File "/home/titiw/.cache/pypoetry/virtualenvs/simpanfile-lylPHfKd-py3.8/lib/python3.8/site-packages/ninja/openapi/schema.py", line 44, in __init__
    ("paths", self.get_paths()),
  File "/home/titiw/.cache/pypoetry/virtualenvs/simpanfile-lylPHfKd-py3.8/lib/python3.8/site-packages/ninja/openapi/schema.py", line 56, in get_paths
    path_methods = self.methods(path_view.operations)
  File "/home/titiw/.cache/pypoetry/virtualenvs/simpanfile-lylPHfKd-py3.8/lib/python3.8/site-packages/ninja/openapi/schema.py", line 67, in methods
    result[method.lower()] = self.operation_details(op)
  File "/home/titiw/.cache/pypoetry/virtualenvs/simpanfile-lylPHfKd-py3.8/lib/python3.8/site-packages/ninja/openapi/schema.py", line 93, in operation_details
    body = self.request_body(operation)
  File "/home/titiw/.cache/pypoetry/virtualenvs/simpanfile-lylPHfKd-py3.8/lib/python3.8/site-packages/ninja/openapi/schema.py", line 149, in request_body
    assert len(models) == 1
AssertionError
stephenrauch commented 3 years ago

https://docs.djangoproject.com/en/3.2/topics/http/file-uploads/ https://stackoverflow.com/q/3111779/7311767

aprilahijriyan commented 3 years ago

Yes, the reference above does work. But can django-ninja do exactly the same as the above reference?

Monstrofil commented 3 years ago

I'm facing this issue too: any combination of UploadedFile with other params gives me "assert len(models) == 1" error.

stephenrauch commented 3 years ago

@Monstrofil, I believe the assert len(models) == 1 is the same issue as in #162.

The first question in this issue needed List[UploadedFile] and is covered here:

https://django-ninja.rest-framework.com/tutorial/file-params/

from typing import List
from ninja import NinjaAPI, File
from ninja.files import UploadedFile

@api.post("/upload-many")
def upload_many(request, files: List[UploadedFile] = File(...)):
    return [f.name for f in files]
vitalik commented 3 years ago

@Monstrofil @aprilahijriyan please check version 0.15

Monstrofil commented 3 years ago

@vitalik yep, works now, thanks.

aprilahijriyan commented 3 years ago

Hi @vitalik, does it support defining form data using pydantic Schema?

class BodySupplierSchema(Schema):
    business_id: UUID
    image: Optional[UploadedFile] = None
    name: constr(max_length=255)
    description: Optional[str] = None
    mobile_phone: Optional[str] = None
    address_details: Optional[str] = None

def create_supplier(request: HttpRequest, body: BodySupplierSchema = Form(...)):
    data = body.dict()
    print(data)
    return {"detail": "success"}

I still get the above error

Screenshot from 2021-09-29 13-58-31

vitalik commented 3 years ago

@aprilahijriyan No, currently you have to pass files as arguments

class BodySupplierSchema(Schema):
    business_id: UUID
    name: constr(max_length=255)
    description: Optional[str] = None
    mobile_phone: Optional[str] = None
    address_details: Optional[str] = None

def create_supplier(request: HttpRequest, body: BodySupplierSchema = Form(...), image: Optional[UploadedFile] = File(None)):
    ...
aprilahijriyan commented 3 years ago

ok, the above code works fine! thank you.

LazavikouArtsiom commented 2 years ago

@aprilahijriyan No, currently you have to pass files as arguments

class BodySupplierSchema(Schema):
    business_id: UUID
    name: constr(max_length=255)
    description: Optional[str] = None
    mobile_phone: Optional[str] = None
    address_details: Optional[str] = None

def create_supplier(request: HttpRequest, body: BodySupplierSchema = Form(...), image: Optional[UploadedFile] = File(None)):
    ...

Hello @vitalik, i have the same schema and args structure

def submit_vacancy_api(
    request,
    cv: UploadedFile,
    payload: schemas.SubmitVacancySchema = Form(...),
    vacancy: schemas.VacancyIdPathSchema = Path(...)
):
class SubmitVacancySchema(Schema):
    message: Optional[str]

and now i have i question!

How could i test file uploading? I've tried this method

data = {
        "message": "Test vacancy submition request message",
        "cv": (io.BytesIO(b"test_bytes"), 'test_cv.pdf')
    }
response = client.post(
            f"{self._endpoint}/{job.pk}/submit",
            data=data,
            content_type='multipart/form-data'
        )

But i got error

b'{"detail": [{"loc": ["file", "cv"], "msg": "field required", "type": "value_error.missing"}]}'

also i tried

        with tempfile.TemporaryFile(suffix=".pdf") as tmp:
            data = submit_vacancy_data()
            response = client.post(
                f"{self._endpoint}/{job.pk}/submit",
                data=data,
                files={'cv': ('test', tmp, 'application/pdf')},
            )

and got the same problem. How to properly pass a file inside?

vitalik commented 2 years ago

@LazavikouArtsiom

something like this:

https://github.com/vitalik/django-ninja/blob/master/tests/test_files.py#L55-L56

from django.core.files.uploadedfile import SimpleUploadedFile

file = SimpleUploadedFile("test.txt", b"data345")
response = client.post("/file2", FILES={"file": file})
LazavikouArtsiom commented 2 years ago

@LazavikouArtsiom

something like this:

https://github.com/vitalik/django-ninja/blob/master/tests/test_files.py#L55-L56

from django.core.files.uploadedfile import SimpleUploadedFile

file = SimpleUploadedFile("test.txt", b"data345")
response = client.post("/file2", FILES={"file": file})

Tnx for answer, will try!

LazavikouArtsiom commented 2 years ago

@LazavikouArtsiom

something like this:

https://github.com/vitalik/django-ninja/blob/master/tests/test_files.py#L55-L56

from django.core.files.uploadedfile import SimpleUploadedFile

file = SimpleUploadedFile("test.txt", b"data345")
response = client.post("/file2", FILES={"file": file})

It works, дзякуй

ArnaudC commented 2 years ago

In my case, I had to read the file in binary mode :

    @cookielogin
    def assert_upload(self, url, file_path, expected_code, content_type="multipart/media-type"):
        with open(file_path, 'rb') as fp:
            binary_content = fp.read()
        simple_uploaded_file = SimpleUploadedFile(file_path, binary_content, content_type=content_type)
        response = self.client.post(url, {'file': simple_uploaded_file})
        self.raise_if_invalid_http_code(response, expected_code)
        return response.json()
LexxLuey commented 7 months ago
class Movie(models.Model):
    class Status_Choices(models.IntegerChoices):
        COMING_UP = 1
        STARTING = 2
        RUNNING = 3
        FINISHED = 4

    name = models.CharField(max_length=200, null=True)
    protagonists = models.CharField(max_length=200, null=True)
    poster = models.ImageField(upload_to ='posters/', null=True)
    trailer = models.FileField(upload_to ='trailers/', null=True)
    start_date = models.DateTimeField(auto_now=False, auto_now_add=False, null=True)
    status = models.IntegerField(choices=Status_Choices.choices, default=Status_Choices.COMING_UP)
    ranking = models.IntegerField(validators=[MinValueValidator(0, message="Cannot have ranking below 0")], default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    modified_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = "movie"
        verbose_name_plural = "movies"

    def __str__(self):
        return self.name

class MovieIn(ModelSchema):
    class Meta:
        model = Movie
        exclude = ["id", "created_at", "modified_at"]
        fields_optional = "__all__"

class MovieOut(ModelSchema):
    class Meta:
        model = Movie
        fields = "__all__"
        fields_optional = "__all__"

class Message(Schema):
    message: str

router = Router()

@router.post("/", response=MovieOut)
def create_movie(
    request,
    payload: MovieIn,
    poster_file: UploadedFile = None,
    trailer_file: UploadedFile = None,
):
    payload_dict = payload.dict()
    movie = Movie(**payload_dict)
    if not poster_file or trailer_file:
        movie.save()
    if poster_file:
        movie.poster.save(movie.name, poster_file)  # will save model instance as well
    if trailer_file:
        movie.trailer.save(movie.name, trailer_file)  # will save model instance as well
    return movie

@router.put("/{movie_id}", response=MovieOut)
def update_movie(
    request,
    movie_id: int,
    payload: MovieIn,
    poster_file: UploadedFile = None,
    trailer_file: UploadedFile = None,
):
    print(movie_id)
    print(payload)
    print(poster_file)
    print(trailer_file)
    movie = get_object_or_404(Movie, id=movie_id)
    for attr, value in payload.dict(exclude_unset=True).items():
        setattr(movie, attr, value)
    movie.save()
    if not poster_file or trailer_file:
        movie.save()
    if poster_file:
        movie.poster.save(movie.name, poster_file)  # will save model instance as well
    if trailer_file:
        movie.trailer.save(movie.name, trailer_file)  # will save model instance as well
    return movie

The POST method works fine whether you send a file or not when trying out the api in the swagger interactive docs. But it fails when it is a PUT method. This is the output I get in my terminal:

Unprocessable Entity: /api/cinema/2
[23/Mar/2024 12:27:48] "PUT /api/cinema/2 HTTP/1.1" 422 86
{
  "detail": [
    {
      "type": "missing",
      "loc": [
        "body",
        "payload"
      ],
      "msg": "Field required"
    }
  ]
}

the curl:

curl -X 'PUT' \
  'http://127.0.0.1:8000/api/cinema/2' \
  -H 'accept: application/json' \
  -H 'Content-Type: multipart/form-data' \
  -F 'poster_file=@GID_1127-DeNoiseAI-standard.jpg;type=image/jpeg' \
  -F 'trailer_file=@IMG-20230101-WA0129 (1).jpg;type=image/jpeg' \
  -F 'payload={
  "name": "3535353"
}'

Any help will be appreciated. Perhaps I am using the ninja wrong?

EDIT: I updated my PUT to this:

@router.put("/{movie_id}", response=MovieOut)
def update_movie(
    request,
    movie_id: int,
    payload: Form[MovieIn],
    poster_file: Optional[UploadedFile] = File(None),
    trailer_file: Optional[UploadedFile] = File(None),
):
    print(movie_id)
    print(payload)
    print(poster_file)
    print(trailer_file)
    movie = get_object_or_404(Movie, id=movie_id)
    for attr, value in payload.dict(exclude_unset=True).items():
        setattr(movie, attr, value)
    if not poster_file or trailer_file:
        movie.save()
    if poster_file:
        movie.poster.save(movie.name, poster_file)  # will save model instance as well
    if trailer_file:
        movie.trailer.save(movie.name, trailer_file)  # will save model instance as well
    return movie

in my terminal i get this:

2
name=None protagonists=None poster=None trailer=None start_date=None status=None ranking=None
None
None

the payload is always empty even though there is data sent from the client.

Any help would be very much appreciated

EarthlyZ9 commented 7 months ago

The POST method works fine whether you send a file or not when trying out the api in the swagger interactive docs. But it fails when it is a PUT method. This is the output I get in my terminal:

Same problem here Any idea why?

EarthlyZ9 commented 7 months ago

@LexxLuey Found this. I guess PUT does not support multipart..? See Roy T. Fielding's comment here

LexxLuey commented 7 months ago

@LexxLuey Found this. I guess PUT does not support multipart..? See Roy T. Fielding's comment here

Interesting. If this is the case then it should be a matter of updating the docs appropriately to let users know how to make an update request in ninja containing files and data.