Using Celery to handle uploads in django

0

I was wondering how I can use Celery workers to handle file uploads. So I tried implementing it on a simple class. I overrided the create class in my ModelViewSet. But apparently Django’s default json encoder does not serialize ImageFields (Lame). I’ll really appreciate it if you guys could tell me how I can fix this. Here is what I came up with:

serializers.py:

class ProductImageSerializer(serializers.ModelSerializer):
    class Meta:
        model = ProductImage
        fields = ['id', 'image']

tasks.py:

from time import sleep
from celery import shared_task
from .models import ProductImage

@shared_task:
def upload_image(product_id, image):
    print('Uploading image...')
    sleep(10)
    product = ProductImage(product_id=product_id, image=image)
    product.save()

views.py:

class ProductImageViewSet(ModelViewSet):
    serializer_class = ProductImageSerializer

    def get_queryset(self):
        return ProductImage.objects.filter(product_id=self.kwargs['product_pk'])

    def create(self, request, *args, **kwargs):
        product_id = self.kwargs['product_pk']
        image = self.request.FILES['image']
        image.open()
        image_data = Image.open(image)
        upload_image.delay(product_id, image_data)

        return Response('Thanks')

and here’s the my model containing my ImageField:

class ProductImage(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='images')
    image = models.ImageField(upload_to='store/images', validators=[validate_image_size])

Hello everyone earlier I posted a solution for this question and even though that solution worked properly, I found a better solution.
Encoding and Decoding binary files using base64 makes them larger and that is not something we want. So a better solution is to temporarily save the uploaded file on the disk, pass the path to our celery worker to upload it and create a ProductImage instance in our database and then delete the file we saved on the disk .

Here’s how to implement it:

tasks.py:

from time import sleep
from celery import shared_task
from .models import ProductImage
from django.core.files import File
from django.core.files.storage import FileSystemStorage
from pathlib import Path

@shared_task
def upload(product_id, path, file_name):

    print('Uploading image...')

    sleep(10)
    
    storage = FileSystemStorage()

    path_object = Path(path)

    with path_object.open(mode='rb') as file:
        
        picture = File(file, name=path_object.name)

        instance = ProductImage(product_id=product_id, image=picture)

        instance.save()


    storage.delete(file_name)

    print('Uploaded!')

In serializers.py you should override the create method of the ProductImage serializer like this:

    def create(self, validated_data):
        product_id = self.context['product_id']
        image_file = self.context['image_file']
        storage = FileSystemStorage()
        
        image_file.name = storage.get_available_name(image_file)

        storage.save(image_file.name, File(image_file))

        return upload.delay(product_id=product_id, path=storage.path(image_file.name), file_name=image_file.name)

You should also override the create method in ProductImage’s ViewSet to provide the image file for your serializer’s context:

    def create(self, request, *args, **kwargs):
        product_id = self.kwargs['product_pk']
        image_file = self.request.FILES['image']
        serializer = ProductImageSerializer(
            data=request.data,
            context={
                'product_id': product_id,
                'image_file': image_file
            }
        )
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response('Upload Started...')