Resolve Django/DRF ImageField URL from F() function - python

I have a use case where I'm attempting to override an Image URL if it exists in our database.
Here is the section of the queryset that is grabbing the ImageField from via an F() query.
preferred_profile_photo=Case(
When(
# agent not exists
Q(agent__id__isnull=False),
then=F("agent__profile__profile_photo"),
),
default=Value(None, output_field=ImageField()),
)
The Case-When is resolving correctly, but the issue is the value returns from F("agent__profile__profile_photo") is not the URL than can be used by the UI. Instead it is something like:
"agentphoto/09bd7dc0-62f6-49ab-blah-6c57b23029d7/profile/1665342685--77e51d9c5asdf345364f774d0b2def48.jpeg"
Typically, I'd retrieve the URL via agent.profile.profile_photo.url, but I receive the following when attempting to perform preferred_profile_photo.url:
AttributeError: 'str' object has no attribute 'url'.
I've tried wrapping in Value(..., output_field=ImageField()) with no luck.
The crux here is retrieving the url from the ImageField after resolving from F()
For reference, I'm using storages.backends.s3boto3.S3Boto3Storage.

I was able to implement this using some information from this post.
Here is the helper function I used to implement:
from django.core.files.storage import get_storage_class
def get_media_storage_url(file_name: str) -> str:
"""Takes the postgres stored ImageField value and converts it to
the proper S3 backend URL. For use cases, where calling .url on the
photo field is not feasible.
Args:
file_name (str): the value of the ImageField
Returns:
str: the full URL of the object usable by the UI
"""
media_storage = get_storage_class()()
return media_storage.url(name=file_name)

Django doesn't store full URL in DB. It builds absolute url on code level. Quote from docs:
All that will be stored in your database is a path to the file (relative to MEDIA_ROOT).
You can use build_absolute_uri() method to get full URL of your files. For example you can do it on serializer level:
class YourSerializer(ModelSerializer):
preferred_profile_photo = serializers.SerializerMethodField()
class Meta:
model = YourModel
fields = [
'preferred_profile_photo',
]
def get_preferred_profile_photo(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(obj.preferred_profile_photo)

Related

Wagtail: How to override default ImageEmbedHandler?

I've been having some trouble implementing Wagtail CMS on my own Django backend. I'm attempting to use the 'headless' version and render content on my own SPA. As a result, I need to create my own EmbedHandlers so that I can generate URL's to documents and images to a private S3 bucket. Unfortunately, though I've registered my own PrivateS3ImageEmbedHandler, Wagtail is still using the default ImageEmbedHandler to convert the html-like bodies to html. Is there a way for me to set it so that Wagtail uses my custom EmbedHandler over the built in default?
Here's my code:
from wagtail.core import blocks, hooks
from messaging.utils import create_presigned_url
class PrivateS3ImageEmbedHandler(EmbedHandler):
identifier = "image"
#staticmethod
def get_model():
return get_user_model()
#classmethod
def get_instance(cls, attrs):
model = cls.get_instance(attrs)
print(model)
return model.objects.get(id=attrs['id'])
#classmethod
def expand_db_attributes(cls, attrs):
image = cls.get_instance(attrs)
print(image)
presigned_url = create_presigned_url('empirehealth-mso', image.file)
print(presigned_url)
return f'<img src="{presigned_url}" alt="it works!"/>'
#hooks.register('register_rich_text_features')
def register_private_images(features):
features.register_embed_type(PrivateS3ImageEmbedHandler)
You need to ensure that your #hooks.register('register_rich_text_features') call happens after the one in the wagtail.images app; this can be done by either putting your app after wagtail.images in INSTALLED_APPS, or by passing an order argument greater than 0:
#hooks.register('register_rich_text_features', order=10)

django-rest-framework HyperlinkedIdentityField with multiple lookup args

I have the following URL in my urlpatterns:
url(r'^user/(?P<user_pk>[0-9]+)/device/(?P<uid>[0-9a-fA-F\-]+)$', views.UserDeviceDetailView.as_view(), name='user-device-detail'),
notice it has two fields: user_pk, and uid. The URL would look something like: https://example.com/user/410/device/c7bda191-f485-4531-a2a7-37e18c2a252c.
In the detail view for this model, I'm trying to populate a url field that will contain the link back to the model.
In the serializer, I have:
url = serializers.HyperlinkedIdentityField(view_name="user-device-detail", lookup_field='uid', read_only=True)
however, it's failing I think because the URL has two fields:
django.core.exceptions.ImproperlyConfigured: Could not resolve URL for hyperlinked relationship using view name "user-device-detail". You may have failed to include the related model in your API, or incorrectly configured the lookup_field attribute on this field.
How do you use a HyperlinkedIdentityField (or any of the Hyperlink*Field) when the URL has two or more URL template items? (lookup fields)?
I'm not sure if you've solved this problem yet, but this may be useful for anyone else who has this issue. There isn't much you can do apart from overriding HyperlinkedIdentityField and creating a custom serializer field yourself. An example of this issue is in the github link below (along with some source code to get around it):
https://github.com/tomchristie/django-rest-framework/issues/1024
The code that is specified there is this:
from rest_framework.relations import HyperlinkedIdentityField
from rest_framework.reverse import reverse
class ParameterisedHyperlinkedIdentityField(HyperlinkedIdentityField):
"""
Represents the instance, or a property on the instance, using hyperlinking.
lookup_fields is a tuple of tuples of the form:
('model_field', 'url_parameter')
"""
lookup_fields = (('pk', 'pk'),)
def __init__(self, *args, **kwargs):
self.lookup_fields = kwargs.pop('lookup_fields', self.lookup_fields)
super(ParameterisedHyperlinkedIdentityField, self).__init__(*args, **kwargs)
def get_url(self, obj, view_name, request, format):
"""
Given an object, return the URL that hyperlinks to the object.
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
kwargs = {}
for model_field, url_param in self.lookup_fields:
attr = obj
for field in model_field.split('.'):
attr = getattr(attr,field)
kwargs[url_param] = attr
return reverse(view_name, kwargs=kwargs, request=request, format=format)
This should work, in your case you would call it like this:
url = ParameterisedHyperlinkedIdentityField(view_name="user-device-detail", lookup_fields=(('<model_field_1>', 'user_pk'), ('<model_field_2>', 'uid')), read_only=True)
Where <model_field_1> and <model_field_2> are the model fields, probably 'id' and 'uid' in your case.
Note the above issue was reported 2 years ago, I have no idea if they've included something like that in newer versions of DRF (I haven't found any) but the above code works for me.

Returning non-predefined fields via. API with Tastypie in Django

I am using Tastypie for non-ORM data source (Amazon Dynamodb). I have gone through the official documentation for non-ORM source and found the following code:
class MessageResource(Resource):
# Just like a Django ``Form`` or ``Model``, we're defining all the
# fields we're going to handle with the API here.
uuid = fields.CharField(attribute='uuid')
user_uuid = fields.CharField(attribute='user_uuid')
message = fields.CharField(attribute='message')
created = fields.IntegerField(attribute='created')
I am new to Tastypie and what I understand is that fields uuid, message, created.. which are returned by API are defined over here. Is there any way that I return those fields that are not defined here i.e. all those fields returned by the dictionary in obj_get_list or obj_get.
You can use the dehydrade method. Simply add a new key to bundle.data.
def dehydrate(self, bundle):
for item in bundle.obj.iteritems():
bundle.data["new_key"] = "new_value"
return bundle

Get model object from tastypie uri?

How do you get the model object of a tastypie modelresource from it's uri?
for example:
if you were given the uri as a string in python, how do you get the model object of that string?
Tastypie's Resource class (which is the guy ModelResource is subclassing ) provides a method get_via_uri(uri, request). Be aware that his calls through to apply_authorization_limits(request, object_list) so if you don't receive the desired result make sure to edit your request in such a way that it passes your authorisation.
A bad alternative would be using a regex to extract the id from your url and then use it to filter through the list of all objects. That was my dirty hack until I got get_via_uri working and I do NOT recommend using this. ;)
id_regex = re.compile("/(\d+)/$")
object_id = id_regex.findall(your_url)[0]
your_object = filter(lambda x: x.id == int(object_id),YourResource().get_object_list(request))[0]
You can use get_via_uri, but as #Zakum mentions, that will apply authorization, which you probably don't want. So digging into the source for that method we see that we can resolve the URI like this:
from django.core.urlresolvers import resolve, get_script_prefix
def get_pk_from_uri(uri):
prefix = get_script_prefix()
chomped_uri = uri
if prefix and chomped_uri.startswith(prefix):
chomped_uri = chomped_uri[len(prefix)-1:]
try:
view, args, kwargs = resolve(chomped_uri)
except Resolver404:
raise NotFound("The URL provided '%s' was not a link to a valid resource." % uri)
return kwargs['pk']
If your Django application is located at the root of the webserver (i.e. get_script_prefix() == '/') then you can simplify this down to:
view, args, kwargs = resolve(uri)
pk = kwargs['pk']
Are you looking for the flowchart? It really depends on when you want the object.
Within the dehydration cycle you simple can access it via bundle, e.g.
class MyResource(Resource):
# fields etc.
def dehydrate(self, bundle):
# Include the request IP in the bundle if the object has an attribute value
if bundle.obj.user:
bundle.data['request_ip'] = bundle.request.META.get('REMOTE_ADDR')
return bundle
If you want to manually retrieve an object by an api url, given a pattern you could simply traverse the slug or primary key (or whatever it is) via the default orm scheme?

FileField in Tastypie

I have a model for which I am making an api in tastypie. I have a field which stores the path to a file which I maintain manually (I am not using FileField since users are not uploading the files). Here is a gist of a model:
class FooModel(models.Model):
path = models.CharField(max_length=255, null=True)
...
def getAbsPath(self):
"""
returns the absolute path to a file stored at location self.path
"""
...
Here is my tastypie config:
class FooModelResource(ModelResource):
file = fields.FileField()
class Meta:
queryset = FooModel.objects.all()
def dehydrate_file(self, bundle):
from django.core.files import File
path = bundle.obj.getAbsPath()
return File(open(path, 'rb'))
In the api in the file field this returns full path to a file. I want tastypie to be able to serve the actual file or at least an url to a file. How do I do that? Any code snippets are appreciated.
Thank you
Decide on a URL scheme how your files will be exposed through the APIs first. You don't really need file or dehydrate_file (unless you want to change the representation of the file for the model itself in Tastypie). Instead just add an additional action on the ModelResource. Example:
class FooModelResource(ModelResource):
file = fields.FileField()
class Meta:
queryset = FooModel.objects.all()
def override_urls(self):
return [
url(r"^(?P<resource_name>%s)/(?P<pk>\w[\w/-]*)/download%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('download_detail'), name="api_download_detail"),
]
def download_detail(self, request, **kwargs):
"""
Send a file through TastyPie without loading the whole file into
memory at once. The FileWrapper will turn the file object into an
iterator for chunks of 8KB.
No need to build a bundle here only to return a file, lets look into the DB directly
"""
filename = self._meta.queryset.get(pk=kwargs[pk]).file
wrapper = FileWrapper(file(filename))
response = HttpResponse(wrapper, content_type='text/plain') #or whatever type you want there
response['Content-Length'] = os.path.getsize(filename)
return response
GET .../api/foomodel/3/
Returns:
{
...
'file' : 'localpath/filename.ext',
...
}
GET .../api/foomodel/3/download/
Returns:
...contents of actual file...
Alternatively you could create a non-ORM Sub Resource file in FooModel. You would have to define resource_uri (how to uniquely identify each instance of the resource), and override dispatch_detail to do exactly what download_detail above does.
The only conversion tastypie does on a FileField is to look for an 'url' attribute on what you return, and return that if it exists, else it will return the string-ized object, which as you have noticed is just the filename.
If you want to return the file content as a field, you will need to handle the encoding of the file. You have a few options:
Simplest: Use CharField and use the base64 module to convert the bytes read from the file into a string
More general but functionally equivalent: write a custom tastypie Serializer that knows how to turn File objects into string representations of their contents
Override the get_detail function of your resource to serve just the file using whatever content-type is appropriate, to avoid the JSON/XML serialization overhead.

Categories