Using drf-nested-routers with nested HyperlinkedIdentityFields - django

I am trying to generate nested HATEOAS links in a serializer using the drf-nested-routes package. My current setup would be as follows:
/resource_a/<pk>
/resource_a/<pk>/resource_b/<pk>
/resource_a/<pk>/resource_b/<pk>
/resource_a/<pk>/resource_b/<pk>/resource_c
I am unable to create a HyperlinkedIdentityField that points to the last route. According to the documentation, one can create hyperlinked fields like this:
nameservers = HyperlinkedIdentityField(
view_name='domain-nameservers-list',
lookup_url_kwarg='domain_pk'
)
Or
nameservers = NestedHyperlinkedRelatedField(
many=True,
read_only=True, # Or add a queryset
view_name='domain-nameservers-detail'
parent_lookup_url_kwargs={'domain_pk': 'domain__pk'}
)
But these approaches fail when trying to reach a resource that is 2 layers deep in the URL hierarchy. The first method is not compatible, as it does not allow to add a second lookup_url_kwarg, and as for the second one, it throws an exception (ImproperlyConfigured) when configuring with the (in my opinion) proper attributes (resource_a__pk, resource_b__pk).
Is this at all possible with this package? Otherwise I will resort to a simpler solution using a SerializerMethodField:
resource_c = serializers.SerializerMethodField()
def get_resource_c(self, obj):
url = reverse('resource_b-resource_c-list', kwargs=dict(resource_a_pk=obj.resource_a.pk, resource_b_pk=obj.pk))
return self.context['request'].build_absolute_uri(url)
Thanks in advance!

I've done this before using NestedHyperlinkedRelatedField and it definitely works. My guess is that your configuration is not correct. One thing I noticed is that you use parent_lookup_url_kwargs while in my case I use parent_lookup_kwargs.
Based on your explanation I think it needs to look like this
NestedHyperlinkedRelatedField(...,
parent_lookup_kwargs={
'resource_a_pk': '<how to reach resource_a pk from resource_b>'})

Related

Django syndication framework: prevent appending SITE_ID to the links

According to the documentation here: https://djangobook.com/syndication-feed-framework/
If link doesn’t return the domain, the syndication framework will
insert the domain of the current site, according to your SITE_ID
setting
However, I'm trying to generate a feed of magnet: links. The framework doesn't recognize this and attempts to append the SITE_ID, such that the links end up like this (on localhost):
<link>http://localhost:8000magnet:?xt=...</link>
Is there a way to bypass this?
Here's a way to do it with monkey patching, much cleaner.
I like to create a separate folder "django_patches" for these kinds of things:
myproject/django_patches/__init__.py
from django.contrib.syndication import views
from django.contrib.syndication.views import add_domain
def add_domain_if_we_should(domain, url, secure=False):
if url.startswith('magnet:'):
return url
else:
return add_domain(domain, url, secure=False)
views.add_domain = add_domain_if_we_should
Next, add it to your INSTALLED_APPS so that you can patch the function.
settings.py
INSTALLED_APPS = [
'django_overrides',
...
]
This is a bit gnarly, but here's a potential solution if you don't want to give up on the Django framework:
The problem is that the method add_domain is buried deep in a huge method within syndication framework, and I don't see a clean way to override it. Since this method is used for both the feed URL and the feed items, a monkey patch of add_domain would need to consider this.
Django source:
https://github.com/django/django/blob/master/django/contrib/syndication/views.py#L178
Steps:
1: Subclass the Feed class you're using and do a copy-paste override of the huge method get_feed
2: Modify the line:
link = add_domain(
current_site.domain,
self._get_dynamic_attr('item_link', item),
request.is_secure(),
)
To something like:
link = self._get_dynamic_attr('item_link', item)
I did end up digging through the syndication source code and finding no easy way to override it and did some hacky monkey patching. (Unfortunately I did it before I saw the answers posted here, all of which I assume will work about as well as this one)
Here's how I did it:
def item_link(self, item):
# adding http:// means the internal get_feed won't modify it
return "http://"+item.magnet_link
def get_feed(self, obj, request):
# hacky way to bypass the domain handling
feed = super().get_feed(obj, request)
for item in feed.items:
# strip that http:// we added above
item['link'] = item['link'][7:]
return feed
For future readers, this was as of Django 2.0.1. Hopefully in a future patch they allow support for protocols like magnet.

Django: How to use django.forms.ModelChoiceField with a Raw SQL query?

I'm trying to render a form with a combo that shows related entities. Therefore I'm using a ModelChoiceField.
This approach works well, until I needed to limit which entities to show. If I use a simple query expression it also works well, but things break if I use a raw SQL query.
So my code that works, sets the queryset to a filter expression.
class ReservationForm(forms.Form):
location_time_slot = ModelChoiceField(queryset=LocationTimeSlot.objects.all(), empty_label="Select your prefered time")
def __init__(self,*args,**kwargs):
city_id = kwargs.pop("city_id") # client is the parameter passed from views.py
super(ReservationForm, self).__init__(*args,**kwargs)
# TODO: move this to a manager
self.fields['location_time_slot'].queryset = LocationTimeSlot.objects.filter(city__id = city_id )
BUT, if I change that to a raw query I start having problems. Code that does not work:
class ReservationForm(forms.Form):
location_time_slot = ModelChoiceField(queryset=LocationTimeSlot.objects.all(), empty_label="Select your prefered time")
def __init__(self,*args,**kwargs):
city_id = kwargs.pop("city_id") # client is the parameter passed from views.py
super(ReservationForm, self).__init__(*args,**kwargs)
# TODO: move this to a manager
query = """SELECT ts.id, ts.datetime_to, ts.datetime_from, ts.available_reserves, l.name, l.'order'
FROM reservations_locationtimeslot AS ts
INNER JOIN reservations_location AS l ON l.id = ts.location_id
WHERE l.city_id = %s
AND ts.available_reserves > 0
AND ts.datetime_from > datetime() """
time_slots = LocationTimeSlot.objects.raw(query, [city_id])
self.fields['location_time_slot'].queryset = time_slots
The first error I get when trying to render the widget is: 'RawQuerySet' object has no attribute 'all'
I could solve that one thanks to one of the commets in enter link description here, by doing:
time_slots.all = time_slots.__iter__ # Dummy fix to allow default form rendering with raw SQL
But now I'm getting something similar when posting the form:
'RawQuerySet' object has no attribute 'get'
Is there a proper way to prepare a RawQuerySet to be used by ModelChoiceField?
Thanks!
Are you sure you actually need a raw query there? Just looking at that query, I can't see any reason you can't just do it with filter(location__city=city_id, available_reserves__gte=0, datetime_from__gt=datetime.datetime.now()).
Raw query sets are missing a number of methods that are defined on conventional query sets, so just dropping them in place isn't likely to work without writing your own definitions for all those methods.
I temporarily fixed the problem adding the missing methods.
The way I'm currently using the ModelChoiceField I only needed to add the all() and get() methods, but in different scenarios you might need to add some other methods as well. Also this is not a perfect solution because:
1) Defining the get method this way migth produce incorrect results. I think the get() method is used to validate that the selected option is within the options returned by all(). The way I temporarily implemented it only validates that the id exists in the table.
2) I guess the get method is less performant specified this way.
If anyone can think of a better solution, please let me know.
So my temporary solution:
class LocationTimeSlotManager(models.Manager):
def availableSlots(self, city_id):
query = """SELECT ts.id, ts.datetime_to, ts.datetime_from, ts.available_reserves, l.name, l.'order'
FROM reservations_locationtimeslot AS ts
.....
.....
MORE SQL """
time_slots = LocationTimeSlot.objects.raw(query, [city_id])
# Dummy fix to allow default form rendering with raw SQL
time_slots.all = time_slots.__iter__
time_slots.get = LocationTimeSlot.objects.get
return time_slots

Django: Querying comments based on object field

I've been using the built-in Django comments system which has been working great. On a particular page I need to list the latest X comments which I've just been fetching with:
latest_comments =
Comment.objects.filter(is_public=True, is_removed=False)
.order_by('submit_date').reverse()[:5]
However I've now introduced a Boolean field 'published' into the parent object of the comments, and I want to include that in the query above. I've tried using the content_type and object_pk fields but I'm not really getting anywhere. Normally you'd do something like:
Comment.objects.filter(blogPost__published=True)
But as it is not stored like that I am not sure how to proceed.
posts_ids = BlogPost.objects.filter(is_published=True).values_list('id', flat=True) #return [3,4,5,...]
ctype = ContentType.objects.get_for_model(BlogPost)
latest_comments = Comment.objects.filter(is_public=True, is_removed=False, content_type=ctype, content_object__in=posts_ids).order_by('-submit_date')[:5]
Comments use GenericForeignKey to store the relation to parent object. Because of the way generic relations work related lookups using __<field> syntax are not supported.
You can accomplish the desired behaviour using the 'in' lookup, however it'll require lot of comparisons when there'll be a lot of BlogPosts.
ids = BlogPost.objects.filter(published=True).values_list('id', flat=True) # Get list of ids, you would probably want to limit number of items returned here
content_type = ContentType.objects.get_for_model(BlogPost) # Becasue we filter only comments for BlogPost
latest_comments = Comment.objects.filter(content_type=content_type, object_pk__in=ids, is_public=True, is_removed=False, ).order_by('submit_date').reverse()[:5]
See the Comment model doc for the description of all fields.
You just cannot do that in one query. Comments use GenericForeignKey. Documentation says:
Due to the way GenericForeignKey is implemented, you cannot use such
fields directly with filters (filter() and exclude(), for example) via
the database API.

How can I get access to a Django Model field verbose name dynamically?

I'd like to have access to one my model field verbose_name.
I can get it by the field indice like this
model._meta._fields()[2].verbose_name
but I need to get it dynamically. Ideally it would be something like this
model._meta._fields()['location_x'].verbose_name
I've looked at a few things but I just can't find it.
For Django < 1.10:
model._meta.get_field_by_name('location_x')[0].verbose_name
model._meta.get_field('location_x').verbose_name
For Django 1.11 and 2.0:
MyModel._meta.get_field('my_field_name').verbose_name
More info in the Django doc
The selected answer gives a proxy object which might look as below.
<django.utils.functional.__proxy__ object at 0x{SomeMemoryLocation}>
If anyone is seeing the same, you can find the string for the verbose name in the title() member function of the proxy object.
model._meta.get_field_by_name(header)[0].verbose_name.title()
A better way to write this would be:
model._meta.get_field(header).verbose_name.title()
where header will be the name of the field you are interested in. i.e., 'location-x' in OPs context.
NOTE: Developers of Django also feel that using get_field is better and thus have depreciated get_field_by_name in Django 1.10. Thus I would suggest using get_field no matter what version of Django you use.
model._meta.get_field_by_name('location_x')[0].verbose_name
You can also use:
Model.location_x.field.verbose_name
Model being the class name. I tested this on my Animal model:
Animal.sale_price.field.verbose_name
Animal.sale_price returns a DeferredAttribute, which has several meta data, like the verbose_name
Note: I'm using Django 3.1.5
If you want to iterate on all the fields you need to get the field:
for f in BotUser._meta.get_fields():
if hasattr(f, 'verbose_name'):
print(f.verbose_name)
# select fields for bulk_update : exclude primary key and relational
fieldsfields_to_update = []
for field_to_update in Model._meta.get_fields():
if not field_to_update.many_to_many and not field_to_update.many_to_one and not field_to_update.one_to_many and not field_to_update.one_to_one and not field_to_update.primary_key and not field_to_update.is_relation :
fields_to_update = fields_to_update + [field_to_update.name]
Model.objects.bulk_update(models_to_update , fields_to_update)

Django's list_details views saving queryset to memory (not updating)?

I have a custom model manager that looks like this:
class MyManager(models.Manager)
def get_query_set(self):
'''Only get items that are 'approved' and have a `pub_date` that is in
the past. Ignore the rest.'''
queryset = super(MyManager, self).get_query_set()
queryset = queryset.filter(status__in=('a',))
return queryset.filter(pub_date__lte=datetime.utcnow())
And this works well enough; however, I have a problem using Django's generic.list_detail views object_detail and object_list: the queryset seems to be only loading once and, because of this, it isn't fetching the items it should be because, I assume, the utcnow() time has been called only once (when it first loaded).
I assume this is intentional and meant as a performance boost - however, it means that video's show up elsewhere on the site (in places I am not in a object_detail view) before they are available in an object_detail view (see urls.py below). This is leading to 404s ...
Any ideas ? Or do I have to write my own custom views to avoid this ?
Thanks!
urls.py
url(r'^video/(?P<object_id>\d+)$',
list_detail.object_detail,
{ 'queryset': Video.objects.all(), },
name='video_detail',
),
It is not a problem of cache: as you do it now, the queryset definition is evaluated once, while parsing urls, and then, it is never evaluated again.
Solution is actually pretty simple and described in the online documentation: Complex filtering with wrapper functions: just create a small custom view, that will simply call the generic view.
I am actually using a similar solution quite a lot and I feel it quite comfortable.
By the way, a small side note, for this case I would suggest not using a custom manager, and go back instead on a normal filtering.
Try correcting urls.py to:
url(r'^video/(?P<object_id>\d+)$',
list_detail.object_detail,
{ 'queryset': Video.objects.all, }, # here's the difference
name='video_detail',
)
Edit:
If this fail, try apply similar technique(passing callable instead of calling it) to filter():
return queryset.filter(pub_date__lte=datetime.utcnow)
I have an almost identical model Manager to thornomad, and the same problem with generic views.
I have to point out that neither of the suggestions above work:
doing Video.objects.all without parentheses gives an error
doing queryset.filter(pub_date__lte=datetime.utcnow), again without the parentheses, does not give an error but does not fix the problem
I have also tried another way, which is to use a lambda to return the queryset, eg:
qs = lambda *x: Video.objects.all()
url(r'^video/(?P<object_id>\d+)$',
list_detail.object_detail,
{ 'queryset': qs(), },
name='video_detail',
),
...it didn't work either and I can see now I must have been desperate to think it would :)
lazy_qs = lambda *x: lazy(Post.live_objects.all, QuerySet)
blog_posts = {
'queryset': lazy_qs(),
...doesn't work either (gives an error) as utils.functional.lazy doesn't know how to convert the result to a QuerySet properly, as best I can tell.
I think Roberto's answer of wrapping the generic view is the only one that will help.
The django docs should be amended to point out the limitations of the queryset used by generic views (currently the docs have a special note to tell you everything will be okay!)