Repository: django-oscar/django-oscar-api (language: python)

Issue:
ImageUrlField is causing an error 
 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? 


Discussion / hints:
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
```js
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`](https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html) 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 of`formData` 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
```py
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](https://www.django-rest-framework.org/api-guide/fields/#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](https://www.django-rest-framework.org/api-guide/fields/#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
https://github.com/django-oscar/django-oscar-api/issues/287#issuecomment-1155342161

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:

```python
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](https://www.django-rest-framework.org/api-guide/fields/#imagefield) on the serializer while we think of a solution.

Fix patch:
diff --git a/oscarapi/serializers/utils.py b/oscarapi/serializers/utils.py
index e0f18139..62672b5c 100644
--- a/oscarapi/serializers/utils.py
+++ b/oscarapi/serializers/utils.py
@@ -13,15 +13,13 @@
 logger = logging.getLogger(__name__)
 
 
-def expand_field_mapping(extra_fields):
-    # This doesn't make a copy
-    field_mapping = serializers.ModelSerializer.serializer_field_mapping
-    field_mapping.update(extra_fields)
-    return field_mapping
+def expand_field_mapping(extra_mapping):
+    field_mapping = serializers.ModelSerializer.serializer_field_mapping.copy()
+    return {**field_mapping, **extra_mapping}
 
 
 class OscarSerializer(object):
-    field_mapping = expand_field_mapping(
+    serializer_field_mapping = expand_field_mapping(
         {
             oscar.models.fields.NullCharField: serializers.CharField,
             models.ImageField: ImageUrlField,


Test patch:
diff --git a/oscarapi/tests/unit/testserializers.py b/oscarapi/tests/unit/testserializers.py
new file mode 100644
index 00000000..d1612b4b
--- /dev/null
+++ b/oscarapi/tests/unit/testserializers.py
@@ -0,0 +1,17 @@
+from django.db import models
+from django.test import TestCase
+from oscarapi.serializers.utils import ImageUrlField
+from rest_framework.fields import ImageField
+
+
+class SerializerstTest(TestCase):
+    def test_drf_image_field_is_not_monkey_patched(self):
+        # see https://github.com/django-oscar/django-oscar-api/issues/287
+        from rest_framework.serializers import ModelSerializer
+
+        default_image_field = ModelSerializer.serializer_field_mapping[
+            models.ImageField
+        ]
+
+        self.assertEqual(default_image_field, ImageField)
+        self.assertNotEqual(default_image_field, ImageUrlField)