4

I've a periodic celery task which needs to store representation of a object in a specific json field.

Here is the simplified model structure. Parent <-- ChildWrapper <-- Child Image

So basically I've a 'ChildImage' model referring to 'ChildWrapper' which in turn refers to 'Parent'.

class Parent(TimeStampedModel):
    label = models.CharField(max_length=30, unique=True)
    live_content = JSONField(blank=True, null=True)
    is_template = models.BooleanField(default=False)
    reference_image = models.ImageField(upload_to=get_web_row_reference_image_path, blank=True, null=True)
    # Around 8 Other Fields

    def __str__(self):
        return '%s' % self.label


class ChildWrapper(TimeStampedModel):
    name = models.CharField(max_length=25, blank=True, null=True)
    row = models.ForeignKey(Parent, on_delete=models.CASCADE, related_name='web_column')
    order = models.PositiveIntegerField(default=0)
    # Around 20 Other Fields

    def __str__(self):
        return '%s' % self.name


class ChildImage(TimeStampedModel):
    image = models.ImageField(upload_to=get_web_image_path)
    column = models.ForeignKey(ChildWrapper, on_delete=models.CASCADE, related_name='web_image')
    # Around 10 Other Fields
 
    def __str__(self):
        return '%s' % self.column

This is the serializers defined for the models.

class ChildImageSerializer(serializers.ModelSerializer):
    class Meta:
        model = ChildImage
        fields = '__all__'

class ChildWrapperSerializer(serializers.ModelSerializer):
    web_image = ChildImageSerializer(read_only=True, many=True)
    class Meta:
        model = ChildWrapper
        fields = '__all__'

class ParentSerializer(serializers.ModelSerializer):
    web_column = ChildWrapperSerializer(many=True, read_only=True)
    class Meta:
        model = Parent
        fields = '__all__'

Here is the periodic celery task which does the required

@app.task(bind=True)
def update_data(self):
    # Get Parent By a condition.
    parent = Parent.objects.filter(to_update=True).first()

    parent.live_content = None
    parent.live_content = ParentSerializer(parent).data
    print(parent.live_content)
    parent.save()

The above task gets output of child image something like this with imagefield being relative path instead of absolute path.

{
    "id": 1
    "image": '/api/col/info.jpg'
}

Is there any way to get the absolute path for the image field?

{
    "id": 1
    "image": "http://localhost:8000/admin/media/api/col/info.jpg"
}

PS: I cannot pass Request context to serializer as ParentSerializer(parent, context={'request': request}) as there is no request object involved here.

Karthik RP
  • 1,028
  • 16
  • 26
  • If you have `django.contrib.sites` installed, you can use `Site.objects.get_current()` to get the hostname (which will be the site you configured in the db). And `settings.MEDIA_URL` is your path prefix. – dirkgroten Aug 19 '19 at 10:25
  • If there is no request object, then you can't access the host so you have to use a pre-defined host and add it to the URL or use one of the allowed hosts in settings. – Navid Zarepak Aug 19 '19 at 10:25

4 Answers4

6

I solved the problem by adding , context={'request': request} in view.

serializer = Business_plansSerializer(business_plans[start:end], many=True, context={'request': request})
Ryan M
  • 18,333
  • 31
  • 67
  • 74
3

I think you have two ways to resolve this.

First one, is to pass request. You can take this approach:

class ChildImageSerializer(serializers.ModelSerializer):
    img_url = serializers.SerializerMethodField()
    class Meta:
        model = ChildImage
        fields = '__all__'

    def get_img_url(self, obj):
        return self.context['request'].build_absolute_uri(obj.image.url)

class ChildWrapperSerializer(serializers.ModelSerializer):
    web_image = serializers.SerializerMethodField()
    class Meta:
        model = ChildWrapper
        fields = '__all__'

    def get_web_image(self, obj):
        serializer_context = {'request': self.context.get('request') }
        children = ChildImage.objects.filter(row=obj)
        serializer = ChildImageSerializer(children, many=True, context=serializer_context)
        return serializer.data

class ParentSerializer(serializers.ModelSerializer):
    web_column = serializers.SerializerMethodField()
    class Meta:
        model = Parent
        fields = '__all__'

    def get_web_column(self, obj):
        serializer_context = {'request': self.context.get('request') }
        children = ChildWrapper.objects.filter(row=obj)
        serializer = ChildWrapperSerializer(children, many=True, context=serializer_context)
        return serializer.data

Here I am using SerializerMethodField to pass the request on to the next serializer.

Second approach is to use Django Sites Framework(mentioned by @dirkgroten). You can do the following:

class ChildImageSerializer(serializers.ModelSerializer):
    img_url = serializers.SerializerMethodField()
    class Meta:
        model = ChildImage
        fields = '__all__'

    def get_img_url(self, obj):
        return 'http://%s%s%s' % (Site.objects.get_current().domain, settings.MEDIA_URL, obj.img.url)

Update: I totally missed the celery part. For production, I don't think you need to worry as they are in S3, the absolute path should be coming from obj.image.url. And in dev and stage, you can get the absolute path using the given example. So, try like this:

class ChildImageSerializer(serializers.ModelSerializer):
    img_url = serializers.SerializerMethodField()
    class Meta:
        model = ChildImage
        fields = '__all__'

    def get_img_url(self, obj):
        if settings.DEBUG:  # debug enabled for dev and stage
            return 'http://%s%s%s' % (Site.objects.get_current().domain, settings.MEDIA_URL, obj.img.url)
        return obj.img.url

Alternatively, there is a way to get request using django-crequest in celery, but I am not sure if its convenient to you.

ruddra
  • 50,746
  • 7
  • 78
  • 101
  • 1
    First process gives me a an error like this 'return self.context['request'].build_absolute_uri(obj.image.url), KeyError: 'request''. Since there is no request object in celery. The second process would have worked if images were stored locally. But they are hosted in aws s3(cloud front) in the live server. Staging and development have stored locally. – Karthik RP Aug 19 '19 at 11:19
  • 1
    @KarthikRP please see the update section of the answer – ruddra Aug 19 '19 at 11:34
  • Will check and update. Seems crequest is for cases where you want to save a request use it somewhere in your code and delete it. Since this is a periodic celery task, there is no request in the first place to set and make use of. – Karthik RP Aug 19 '19 at 11:37
3

Got it working,

Added MEDIA_URL to my settings file as mentioned here.

It seems DRF uses MEDIA_URL as a default prefix for urls(FileField & ImageField), even for non request/response flows.

Since I had a different settings file for staging, development and production it was easier for me to set different URLs for each environment.

Even though I'm not using 'django-versatileimagefield' library, the suggestion there still worked.

Karthik RP
  • 1,028
  • 16
  • 26
-1

Another solution is hard code the host:

from django.conf import settings

IMG_HOST = {
    '/home/me/path/to/project': 'http://localhost:8000',
    '/home/user/path/to/project': 'https://{AWS_HOST}',
}[str(settings.BASE_DIR)]

class ChildImageSerializer(serializers.ModelSerializer):
    image = serializers.SerializerMethodField()

    def get_image(self, obj):
        if obj.image:
            return IMG_HOST + obj.image.url

    class Meta:
        model = ChildImage
        fields = '__all__'
Waket Zheng
  • 5,065
  • 2
  • 17
  • 30
  • Nice., Doesn't help me though. I've a development server, staging server which has DEBUG set to True, and the production the media is hosted in AWS S3(Cloud front) – Karthik RP Aug 19 '19 at 11:55