17

I have 2 models Task and TaskImage which is a collection of images belonging to Task object.

What I want is to be able to add multiple images to my Task object, but I can only do it using 2 models. Currently, when I add images, it doesn't let me upload them and save new objects.

settings.py

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

serializers.py

class TaskImageSerializer(serializers.ModelSerializer):
    class Meta:
        model = TaskImage
        fields = ('image',)


class TaskSerializer(serializers.HyperlinkedModelSerializer):
    user = serializers.ReadOnlyField(source='user.username')
    images = TaskImageSerializer(source='image_set', many=True, read_only=True)

    class Meta:
        model = Task
        fields = '__all__'

    def create(self, validated_data):
        images_data = validated_data.pop('images')
        task = Task.objects.create(**validated_data)
        for image_data in images_data:
            TaskImage.objects.create(task=task, **image_data)
        return task

models.py

class Task(models.Model):
    title = models.CharField(max_length=100, blank=False)
    user = models.ForeignKey(User)

    def save(self, *args, **kwargs):
        super(Task, self).save(*args, **kwargs)

class TaskImage(models.Model):
    task = models.ForeignKey(Task, on_delete=models.CASCADE)
    image = models.FileField(blank=True)

However, when I do a post request:

enter image description here

I get the following traceback:

File "/Applications/Anaconda/anaconda/envs/godo/lib/python3.6/site-packages/django/core/handlers/exception.py" in inner 41. response = get_response(request)

File "/Applications/Anaconda/anaconda/envs/godo/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response 187. response = self.process_exception_by_middleware(e, request)

File "/Applications/Anaconda/anaconda/envs/godo/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response 185. response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/Applications/Anaconda/anaconda/envs/godo/lib/python3.6/site-packages/django/views/decorators/csrf.py" in wrapped_view 58. return view_func(*args, **kwargs)

File "/Applications/Anaconda/anaconda/envs/godo/lib/python3.6/site-packages/rest_framework/viewsets.py" in view 95. return self.dispatch(request, *args, **kwargs)

File "/Applications/Anaconda/anaconda/envs/godo/lib/python3.6/site-packages/rest_framework/views.py" in dispatch 494. response = self.handle_exception(exc)

File "/Applications/Anaconda/anaconda/envs/godo/lib/python3.6/site-packages/rest_framework/views.py" in handle_exception 454. self.raise_uncaught_exception(exc)

File "/Applications/Anaconda/anaconda/envs/godo/lib/python3.6/site-packages/rest_framework/views.py" in dispatch 491. response = handler(request, *args, **kwargs)

File "/Applications/Anaconda/anaconda/envs/godo/lib/python3.6/site-packages/rest_framework/mixins.py" in create 21. self.perform_create(serializer)

File "/Users/gr/Desktop/PycharmProjects/godo/api/views.py" in perform_create 152. serializer.save(user=self.request.user)

File "/Applications/Anaconda/anaconda/envs/godo/lib/python3.6/site-packages/rest_framework/serializers.py" in save 214. self.instance = self.create(validated_data)

File "/Users/gr/Desktop/PycharmProjects/godo/api/serializers.py" in create 67. images_data = validated_data.pop('images')

Exception Type: KeyError at /api/tasks/ Exception Value: 'images'

GRS
  • 2,807
  • 4
  • 34
  • 72

2 Answers2

23

Description for the issue

The origin of the exception was a KeyError, because of this statement

images_data = validated_data.pop('images')

This is because the validated data has no key images. This means the images input doesn't validate the image inputs from postman.

Django post request store InMemmoryUpload in request.FILES, so we use it for fetching files. also, you want multiple image upload at once. So, you have to use different image_names while your image upload (in postman).

Change your serializer to like this:

class TaskSerializer(serializers.HyperlinkedModelSerializer):
    user = serializers.ReadOnlyField(source='user.username')
    images = TaskImageSerializer(source='taskimage_set', many=True, read_only=True)

    class Meta:
        model = Task
        fields = ('id', 'title', 'user', 'images')

    def create(self, validated_data):
        images_data = self.context.get('view').request.FILES
        task = Task.objects.create(title=validated_data.get('title', 'no-title'),
                                   user_id=1)
        for image_data in images_data.values():
            TaskImage.objects.create(task=task, image=image_data)
        return task

I don't know about your view, but I'd like to use ModelViewSet preferrable view class

class Upload(ModelViewSet):
    serializer_class = TaskSerializer
    queryset = Task.objects.all()

Postman console:

enter image description here

DRF result:

{
        "id": 12,
        "title": "This Is Task Title",
        "user": "admin",
        "images": [
            {
                "image": "http://127.0.0.1:8000/media/Screenshot_from_2017-12-20_07-18-43_tNIbUXV.png"
            },
            {
                "image": "http://127.0.0.1:8000/media/game-of-thrones-season-valar-morghulis-wallpaper-1366x768_3bkMk78.jpg"
            },
            {
                "image": "http://127.0.0.1:8000/media/IMG_212433_lZ2Mijj.jpg"
            }
        ]
    }

UPDATE

This is the answer for your comment.

In django reverse foreignKey are capturing using _set. see this official doc. Here, Task and TaskImage are in OneToMany relationship, so if you have one Task instance, you could get all related TaskImage instance by this reverse look-up feature.

Here is the example:

task_instance = Task.objects.get(id=1)
task_img_set_all = task_instance.taskimage_set.all()

Here this task_img_set_all will be equal to TaskImage.objects.filter(task_id=1)

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
JPG
  • 82,442
  • 19
  • 127
  • 206
  • You can use this git-repo if you want https://bitbucket.org/jerinpetergeorge/django_1_11/src/97728beb7b7979464f98ce50d9c70e7525a948ff/?at=SO-48756249 – JPG Feb 13 '18 at 09:32
  • Thank you, this is perfect. I notice that in `serializers` you use `source=taskimage_set`, I'm not sure why this is so? It would make sense to be `task_image_set` or more generally, `model-field_set`, is this just a convention for use for one-to-many relationships? – GRS Feb 13 '18 at 09:43
  • 1
    Please have a look at **UPDATE** section at bottom of the answer, – JPG Feb 13 '18 at 09:58
  • 3
    When you create the TaskImage (```TaskImage.objects.create(task=task, image=image_data)```), should this not be created with the serializer? As without doing so, I would assume you are bypassing the validation stage, and saving the object without validation? – geekscrap Mar 05 '19 at 00:34
  • Not really, because the images are validated against the `serializer.ImageField()` field – JPG Mar 05 '19 at 00:40
  • I found a solution to a scenario where you can upload multiple images with form-data and send nested json. You can examine my [answer](https://stackoverflow.com/a/61179429/8569342) . – Metehan Gülaç Apr 13 '20 at 14:38
1

You have read_only set to true in TaskImageSerializer nested field. So there will be no validated_data there.

kevswanberg
  • 2,079
  • 16
  • 21