django-oscar/django-oscar-api

ImageUrlField is causing an error

Opened this issue · 13 comments

when I try to create a category with an image I get the following error:

Environment:`

Request Method: PUT
Request URL: http://127.0.0.1:8000/api/admin/categories/8/

Django Version: 3.2.12
Python Version: 3.8.10
Installed Applications:
['django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.flatpages',
'oscar.config.Shop',
'oscar.apps.analytics.apps.AnalyticsConfig',
'oscar.apps.checkout.apps.CheckoutConfig',
'oscar.apps.address.apps.AddressConfig',
'oscar.apps.shipping.apps.ShippingConfig',
'oscar.apps.catalogue.apps.CatalogueConfig',
'oscar.apps.catalogue.reviews.apps.CatalogueReviewsConfig',
'oscar.apps.communication.apps.CommunicationConfig',
'oscar.apps.partner.apps.PartnerConfig',
'oscar.apps.basket.apps.BasketConfig',
'oscar.apps.payment.apps.PaymentConfig',
'oscar.apps.offer.apps.OfferConfig',
'oscar.apps.order.apps.OrderConfig',
'oscar.apps.customer.apps.CustomerConfig',
'oscar.apps.search.apps.SearchConfig',
'oscar.apps.voucher.apps.VoucherConfig',
'oscar.apps.wishlists.apps.WishlistsConfig',
'oscar.apps.dashboard.apps.DashboardConfig',
'oscar.apps.dashboard.reports.apps.ReportsDashboardConfig',
'oscar.apps.dashboard.users.apps.UsersDashboardConfig',
'oscar.apps.dashboard.orders.apps.OrdersDashboardConfig',
'oscar.apps.dashboard.catalogue.apps.CatalogueDashboardConfig',
'oscar.apps.dashboard.offers.apps.OffersDashboardConfig',
'oscar.apps.dashboard.partners.apps.PartnersDashboardConfig',
'oscar.apps.dashboard.pages.apps.PagesDashboardConfig',
'oscar.apps.dashboard.ranges.apps.RangesDashboardConfig',
'oscar.apps.dashboard.reviews.apps.ReviewsDashboardConfig',
'oscar.apps.dashboard.vouchers.apps.VouchersDashboardConfig',
'oscar.apps.dashboard.communications.apps.CommunicationsDashboardConfig',
'oscar.apps.dashboard.shipping.apps.ShippingDashboardConfig',
'widget_tweaks',
'haystack',
'treebeard',
'sorl.thumbnail',
'easy_thumbnails',
'django_tables2',
'django.contrib.sitemaps',
'django_extensions',
'debug_toolbar',
'oscarapi',
'rest_framework']
Installed Middleware:
['debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'django.middleware.common.CommonMiddleware',
'oscar.apps.basket.middleware.BasketMiddleware',
'oscarapi.middleware.HeaderSessionMiddleware']

Traceback (most recent call last):
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 181, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/lib/python3.8/contextlib.py", line 75, in inner
return func(*args, **kwds)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/django/views/generic/base.py", line 70, in view
return self.dispatch(request, *args, **kwargs)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/rest_framework/views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/rest_framework/generics.py", line 285, in put
return self.update(request, *args, **kwargs)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/rest_framework/mixins.py", line 67, in update
serializer.is_valid(raise_exception=True)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/rest_framework/serializers.py", line 227, in is_valid
self._validated_data = self.run_validation(self.initial_data)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/rest_framework/serializers.py", line 426, in run_validation
value = self.to_internal_value(data)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/rest_framework/serializers.py", line 483, in to_internal_value
validated_value = field.run_validation(primitive_value)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/rest_framework/fields.py", line 568, in run_validation
value = self.to_internal_value(data)
File "/home/mhb/Documents/django-oscar/venv/lib/python3.8/site-packages/oscarapi/serializers/fields.py", line 332, in to_internal_value
http_prefix = data.startswith(("http:", "https:"))

Exception Type: AttributeError at /api/admin/categories/8/
Exception Value: 'InMemoryUploadedFile' object has no attribute 'startswith'

can someone also tell me what is the purpose of this ImageUrlField and why it replaces the rest_frameworks ImageField?

It is there so you can also provide an external url so oscarapi will download this instead of providing the image itself.

Can you provide the request data as well?

@tofubit See my question above

I'm facing the same issue too, data in this context seem to be a InMemoryUploadedFile type object (or TemporaryUploadedFile if it was more than 2.5MB).
neither of which has the method startWith.

I had a working api before adding django-oscar and django-oscar-api and by just adding them it stared to crash on this same line.

I'm sending the request image as a form data from my frontend code

const formData = new FormData();

const headers = { "Content-Type": "multipart/form-data" };
formData.append("img", blob, file_name);

return await $axios.post(url, formData, { headers });

if I print the request.data inside django-oscar-api I get this

<QueryDict: {'img': [<InMemoryUploadedFile: photo-1598411072028-c4642d98352c.jpeg (image/jpeg)>]}>

note that I'm also using django-storages to upload the images to s3.

is there a way to at least disable this override ?

@maerteijn if there are any more info you'd like I'd be happy to provide them.

Can you provide the url you are posting to (url in your example), the complete contents offormData and the Django traceback?

I couldn't share the project that I was working on, but I created a minimal project that shows this issue and deployed it on heroku.

https://oscar-api-test.herokuapp.com/mapi/store-image/
if you go to this url and use the DRF form, you can get the same error

the project is here
https://github.com/Jimmar/oscarApiTest

if I just remove the line

path("api/", include("oscarapi.urls")),

from urls.py then this issue goes away and the upload works fine

the ImageUrlField is there because there is no multipart form-data in a json api. So you can't upload images you have to put a url of an image there, oscarapi will download the image from that url.

Maybe you are trying to use oscarapi to build a javascript frontend for the admin maybe? In that case you need to implement your own api for uploading images. the admin api is build to synchronise data to oscar from an external application via json/rest.

@specialunderwear I'm not even trying to use oscar api yet.

I currently have an API endpoint that works fine to upload images, just adding oscar api to the project breaks that.
I'm not trying to use oscar api to upload images.

in the project that I just linked, if you removed that line from the urls.py then the image upload works fine.

@specialunderwear I'm not even trying to use oscar api yet.

I currently have an API endpoint that works fine to upload images, just adding oscar api to the project breaks that. I'm not trying to use oscar api to upload images.

in the project that I just linked, if you removed that line from the urls.py then the image upload works fine.

@Jimmar So what you are saying is that by just installing / enabling django-oscar-api, the regular Django Rest Framework ImageField gets broken?

@specialunderwear I'm not even trying to use oscar api yet.
I currently have an API endpoint that works fine to upload images, just adding oscar api to the project breaks that. I'm not trying to use oscar api to upload images.
in the project that I just linked, if you removed that line from the urls.py then the image upload works fine.

@Jimmar So what you are saying is that by just installing / enabling django-oscar-api, the regular Django Rest Framework ImageField gets broken?

basically yes, doesn't to work well with formData, the serializer is expecting data to be a string url but it's actually being handled as an InMemoryUploadedFile (or TemporaryUploadedFile if it was more than 2.5MB).

if you take a look at this comment that I wrote here
#287 (comment)

it has a link to a sample project that I made to showcase this issue and a url to test it (deployed on Heroku)

Ok confirmed,

This is because we patch rest framework to have the ImageUrlField as a default field for models.ImageField. This happens in the OscarSerializer and is implicitly monkey patching DRF:

def expand_field_mapping(extra_fields):
    # This doesn't make a copy
    field_mapping = serializers.ModelSerializer.serializer_field_mapping   # <-- monkeypaych
    field_mapping.update(extra_fields)
    return field_mapping


class OscarSerializer(object):
    field_mapping = expand_field_mapping(
        {
            oscar.models.fields.NullCharField: serializers.CharField,
            models.ImageField: ImageUrlField,
        }
    )

This, of course, we shouldn't do.

Fastest solution for now: explicity define the DRF ImageField on the serializer while we think of a solution.

@Jimmar We committed a fix to the main branch. Could you install the package from github in your local environment:

pip install git+https://github.com/django-oscar/django-oscar-api.git

and verify this fix works for your scenario?

@Jimmar We committed a fix to the main branch. Could you install the package from github in your local environment:

pip install git+https://github.com/django-oscar/django-oscar-api.git

and verify this fix works for your scenario?

I confirm that this fixes the issue, thank you !