Django Admin, sort with custom function - python

How can I sort a column in Django admin by some simple custom method?
(All the answers I got through was by using annotate but I don't know how to use it my case).
Assume the model
class Shots(models.Model):
hits = models.PositiveIntegerField()
all = models.PositiveIntegerField()
In admin site I would like to sort by hits to all ratio:
class ShotAdmin(admin.ModelAdmin):
list_display = ['ratio']
def ratio(self, obj):
return obj.hits / obj.all * 100
I know the ratio is not a field in DB thus simple ratio.admin_order_field = 'ratio' won't work and I need to somehow append this as a field but I have no idea how.

By following:
The accepted answer to this post:
Django admin: how to sort by one of the custom list_display fields that has no database field
The
How to execute arithmetic operations between Model fields in django
(Disclaimer: I have composed that Q&A style example)
We can compose a solution to your problem:
from django.db.models import F
class ShotsAdmin(admin.ModelAdmin):
list_display = ('get_ratio',)
def get_queryset(self, request):
qs = super(ShotsAdmin, self).get_queryset(request)
qs = qs.annotate(ratio=F('hits') * 100 / F('all'))
return qs
def get_ratio(self, obj):
return obj.ratio
get_ratio.admin_order_field = 'ratio'
Explanation:
The get_queryset method will annotate a new field named ratio to
your queryset. That field's value is the application of your ratio function on the hits and all fields.
The get_ratio function returns the aforementioned field from a queryset instance.
Finally: get_ratio.admin_order_field = 'ratio' sets the ratio field as the ordering field for your queryset on the admin panel.

Related

Limiting choices in foreign key dropdown in Django using Generic Views ( CreateView )

I've two models:
First one:
class A(models.Model):
a_user = models.ForeignKey(User, unique=False, on_delete=models.CASCADE)
a_title = models.CharField("A title", max_length=500)
Second one:
class B(models.Model):
b_a = models.ForeignKey(A, verbose_name=('A'), unique=False, on_delete=models.CASCADE)
b_details = models.TextField()
Now, I'm using CreateView to create form for Value filling :
class B_Create(CreateView):
model = B
fields = ['b_a','b_details']
Then using this to render these field in templates.
Now, my problem is, while giving the field b_a ( which is the dropdown ), it list downs all the values of model A, but the need is to list only the values of model A which belongs to the particular logged in user, in the dropdown.
I've seen all the answers, but still not able to solve the problem.
The things I've tried:
limit_choices_to in models : Not able to pass the value of A in the limit_choices
form_valid : Don't have the model A in the CreateView, as only B is reffered model in B_Create
passing primary key of A in templates via url : Then there is no instance of A in the template so can't access. Also, don't want to handle it in templates.
I'm new to Django and still learning, so don't know to override admin form.
Please suggest the implemented way, if possible to the problem. I've researched and tried most of the similar questions with no result for my particular problem. I feel like, this is a dumb question to ask, but I'm stuck here, so need help.
Thanks..
(Please feel free to suggest corrections.)
You have access to self.request.user in the form_valid of the view. But in order to limit the choices in the form you have to customize the form before it is served initially. You best override the view's get_form and set the form field's queryset:
class B_Create(CreateView):
model = B
fields = ['b_a','b_details']
def get_form(self, *args, **kwargs):
form = super(B_Create, self).get_form(*args, **kwargs)
form.fields['b_a'].queryset = self.request.user.a_set.all()
# form.fields['b_a'].queryset = A.objects.filter(a_user=self.request.user)
return form
Generally, there are three places where you can influence the choices of a ModelChoiceField:
If the choices need no runtime knowledge of your data, user, or form instance, and are the same in every context where a modelform might be used, you can set limit_choices_to on the ForeignKey field itself; as module level code, this is evaluated once at module import time. The according query will be built and executed every time a form is rendered.
If the choices need no runtime knowledge, but might be different in different forms, you can use custom ModelForms and set the queryset in the field definition of the respective form field.
If the queryset needs any runtime information, you can either override the __init__ of a custom form and pass it any information it needs to set the field's queryset or you just modify the queryset on the form after it is created which often is a quicker fix and django's default views provide nice hooks to do that (see the code above).
The #schwobaseggl answer is excellent.
Here is a Python 3 version. I needed to limit the projects dropdown input based on the logged-in user.
class ProductCreateView(LoginRequiredMixin, CreateView):
model = Product
template_name = 'brand/product-create.html'
fields = '__all__'
def get_form(self, form_class=None):
form = super().get_form(form_class=None)
form.fields['project'].queryset = form.fields['project'].queryset.filter(owner_id=self.request.user.id)
return form

Problems with cache queryset django rest framework

Good day to all, I have been trying to make this query be consulted every time the REST service is used in the API, but only the first time it obtains the data from the DB and when the data changes the service only brings the cache stored data
My Code:
urls.py
router.register(r'cron-log',views.CronLogViewSet, base_name='cron-log')
Views.py - my viewset class
class CronLogViewSet(viewsets.ModelViewSet):
queryset = Cron_log.objects.all().order_by('-id').values()[:5:1]
serializer_class = CronLogSerializer
Models.py my model class from Cron_log
class Cron_log(models.Model):
log = models.CharField(max_length=40)
time = models.CharField(max_length=40)
def as_dict(self):
return {'log':self.log,'time':self.time}
Serializer.py serializer class
class CronLogSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Cron_log
fields = ('log','time')
I tried with a for before the queryset, with list(query_set) but the issue still there thanks! for your help
The QuerySet is not updating for a couple reasons. 1. It is being evaluated with a slice that uses the step parameter (the step parameter is the 1 in [:5:1] (which is superfluous as 1 is the default - unless you want to trigger an evaluation)). 2. It is evaluated only once in the class body definition which does not update every time an instance of the viewset is initialized. See the Django documentation's section on when querysets are evaluated for more information regarding slicing.
If you want the viewset to always return an updated queryset of objects limited to the first 5 ordered by descending id, one option is to place the limit slice in a get_queryset instance method that returns the queryset limited to the first 5 results.
class CronLogViewSet(viewsets.ModelViewSet):
queryset = Cron_log.objects.all().order_by('-id')
serializer_class = CronLogSerializer
def get_queryset(self):
queryset = super(CronLogViewSet, self).get_queryset()
return queryset.values()[:5]
See the Django Rest Framework documentation's section on filtering and, more specifically, the implementation of views.GenericAPIView.get_queryset for more information.

How to use custom field for search in django admin

i have a model and is registered to the admin and i have used custom field to display in the list
class ReportsAdmin(admin.ModelAdmin):
def investment(self, inst):
return models.OrderDetail.objects.filter(user=inst.user).distinct().count()
list_display = ['investment']
search_fields = ['investment']
i want to search using the investment field in django admin but always getting Cannot resolve keyword 'investment' into field. choices are the Model fields.
is there any way by which i can search using the investment field?
In software, anything is possible... SMH at the accepted answer. You have to override get_search_results.
from django.db.models import Count
class ReportsAdmin(admin.ModelAdmin):
def investment(self, inst):
return models.OrderDetail.objects.filter(user=inst.user).distinct().count()
list_display = ['investment']
search_fields = ['investment']
def get_search_results(self, request, queryset, search_term):
# search_term is what you input in admin site
search_term_list = search_term.split(' ') #['apple','bar']
if not any(search_term_list):
return queryset, False
if 'investment' in search_term_list:
queryset = OrderDetail.objects.annotate(
user_count=Count('user')
).filter(user_count__gte=search_term_list['investment'])
return queryset, False
Well, this is not allowed:
ModelAdmin.search_fields
Set search_fields to enable a search box on the admin change list
page. This should be set to a list of field names that will be
searched whenever somebody submits a search query in that text box.
These fields should be some kind of text field, such as CharField or
TextField. You can also perform a related lookup on a ForeignKey or
ManyToManyField with the lookup API “follow” notation:
You don't have such a field at all (never mind that the field has to be a TextField or CharField). What you actually have is a method in your admin class, which cannot be searched at the database level. Ie what's in the search_fields translates to like '%search_term%' type queries executed at the db.

Optimizing database queries in Django REST framework

I have the following models:
class User(models.Model):
name = models.Charfield()
email = models.EmailField()
class Friendship(models.Model):
from_friend = models.ForeignKey(User)
to_friend = models.ForeignKey(User)
And those models are used in the following view and serializer:
class GetAllUsers(generics.ListAPIView):
authentication_classes = (SessionAuthentication, TokenAuthentication)
permission_classes = (permissions.IsAuthenticated,)
serializer_class = GetAllUsersSerializer
model = User
def get_queryset(self):
return User.objects.all()
class GetAllUsersSerializer(serializers.ModelSerializer):
is_friend_already = serializers.SerializerMethodField('get_is_friend_already')
class Meta:
model = User
fields = ('id', 'name', 'email', 'is_friend_already',)
def get_is_friend_already(self, obj):
request = self.context.get('request', None)
if request.user != obj and Friendship.objects.filter(from_friend = user):
return True
else:
return False
So basically, for each user returned by the GetAllUsers view, I want to print out whether the user is a friend with the requester (actually I should check both from_ and to_friend, but does not matter for the question in point)
What I see is that for N users in database, there is 1 query for getting all N users, and then 1xN queries in the serializer's get_is_friend_already
Is there a way to avoid this in the rest-framework way? Maybe something like passing a select_related included query to the serializer that has the relevant Friendship rows?
Django REST Framework cannot automatically optimize queries for you, in the same way that Django itself won't. There are places you can look at for tips, including the Django documentation. It has been mentioned that Django REST Framework should automatically, though there are some challenges associated with that.
This question is very specific to your case, where you are using a custom SerializerMethodField that makes a request for each object that is returned. Because you are making a new request (using the Friends.objects manager), it is very difficult to optimize the query.
You can make the problem better though, by not creating a new queryset and instead getting the friend count from other places. This will require a backwards relation to be created on the Friendship model, most likely through the related_name parameter on the field, so you can prefetch all of the Friendship objects. But this is only useful if you need the full objects, and not just a count of the objects.
This would result in a view and serializer similar to the following:
class Friendship(models.Model):
from_friend = models.ForeignKey(User, related_name="friends")
to_friend = models.ForeignKey(User)
class GetAllUsers(generics.ListAPIView):
...
def get_queryset(self):
return User.objects.all().prefetch_related("friends")
class GetAllUsersSerializer(serializers.ModelSerializer):
...
def get_is_friend_already(self, obj):
request = self.context.get('request', None)
friends = set(friend.from_friend_id for friend in obj.friends)
if request.user != obj and request.user.id in friends:
return True
else:
return False
If you just need a count of the objects (similar to using queryset.count() or queryset.exists()), you can include annotate the rows in the queryset with the counts of reverse relationships. This would be done in your get_queryset method, by adding .annotate(friends_count=Count("friends")) to the end (if the related_name was friends), which will set the friends_count attribute on each object to the number of friends.
This would result in a view and serializer similar to the following:
class Friendship(models.Model):
from_friend = models.ForeignKey(User, related_name="friends")
to_friend = models.ForeignKey(User)
class GetAllUsers(generics.ListAPIView):
...
def get_queryset(self):
from django.db.models import Count
return User.objects.all().annotate(friends_count=Count("friends"))
class GetAllUsersSerializer(serializers.ModelSerializer):
...
def get_is_friend_already(self, obj):
request = self.context.get('request', None)
if request.user != obj and obj.friends_count > 0:
return True
else:
return False
Both of these solutions will avoid N+1 queries, but the one you pick depends on what you are trying to achieve.
Described N+1 problem is a number one issue during Django REST Framework performance optimization, so from various opinions, it requires more solid approach then direct prefetch_related() or select_related() in get_queryset() view method.
Based on collected information, here's a robust solution that eliminates N+1 (using OP's code as an example). It's based on decorators and slightly less coupled for larger applications.
Serializer:
class GetAllUsersSerializer(serializers.ModelSerializer):
friends = FriendSerializer(read_only=True, many=True)
# ...
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related("friends")
return queryset
Here we use static class method to build the specific queryset.
Decorator:
def setup_eager_loading(get_queryset):
def decorator(self):
queryset = get_queryset(self)
queryset = self.get_serializer_class().setup_eager_loading(queryset)
return queryset
return decorator
This function modifies returned queryset in order to fetch related records for a model as defined in setup_eager_loading serializer method.
View:
class GetAllUsers(generics.ListAPIView):
serializer_class = GetAllUsersSerializer
#setup_eager_loading
def get_queryset(self):
return User.objects.all()
This pattern may look like an overkill, but it's certainly more DRY and has advantage over direct queryset modification inside views, as it allows more control over related entities and eliminates unnecessary nesting of related objects.
Using this metaclass DRF optimize ModelViewSet MetaClass
from django.utils import six
#six.add_metaclass(OptimizeRelatedModelViewSetMetaclass)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
You can split the view into two query.
First, only get the Users list (without is_friend_already field). This only require one query.
Second, get the friends list of request.user.
Third, modify the results depending on if the user is in the request.user's friend list.
class GetAllUsersSerializer(serializers.ModelSerializer):
...
class UserListView(ListView):
def get(self, request):
friends = request.user.friends
data = []
for user in self.get_queryset():
user_data = GetAllUsersSerializer(user).data
if user in friends:
user_data['is_friend_already'] = True
else:
user_data['is_friend_already'] = False
data.append(user_data)
return Response(status=200, data=data)

Override queryset filters while generating django admin URLs

My models.py looks like this:
class Person(models.Model):
Name = models.CharField(max_length=100)
class Lecture(models.Model):
Speaker = model.ForeignKey(Person)
Topic = models.CharField(max_length=100)
Choices = ((1,"Upcoming"),(2,"In Progress",),(3,"Completed"))
Status = models.SmallIntegerField(choices=Choices, default=1, max_length=1)
My admin.py looks like this:
class LectureAdmin(admin.ModelAdmin):
def get_queryset(self):
return Lecture.objects.exclude(Status='Completed')
So my change list view in the django admin for the Lecture model shows only Lectures in "Upcoming" and "In Progress" status. This works fine.
Now I need to get the URL for the list of all lectures to be passed as a view somewhere else.The standard way of doing this in the django admin is by reversing the URL, so I do this:
urlresolvers.reverse('admin:%s_%s_changelist' % (app_label, model_name))
However, when I do this,I get the the filtered Queryset with Lectures in "Completed" state missing.How do I construct a url reverse function to get entire Lecture queryset and not the filtered queryset?
Here's a workaround, looks ugly, I understand.
Add all GET parameter to the changelist url:
url = urlresolvers.reverse('admin:%s_%s_changelist' % (app_label, model_name))
url += '?all'
Call get_queryset() on super(), exclude Completed status only if there is no all in request.GET:
class LectureAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super(LectureAdmin, self).get_queryset(request)
if 'all' not in request.GET:
qs = qs.exclude(Status='Completed')
return qs
UPD (applying other filters from request.GET):
from xadmin.plugin.related import RELATE_PREFIX # or just set RELATE_PREFIX = '_rel_'
qs = qs.filter(**{key[len(RELATE_PREFIX):]: value
for key, value in request.GET.iteritems()
if key.startswith(RELATE_PREFIX)})
** unpacks the dictionary into keyword arguments.
Hope it works for you.
get_queryset() is the basic queryset used in admin listing, thus you wo'nt be able to get all the records if you override it this way.
Possible solutions:
use filters ( https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.filter_vertical ) to exclude unwanted records (these with Status='Completed'
or
create proxy model for Lecture, register it in admin and use modified get_queryset() in given listing. Proxy model is required because each model can have registered only single AdminModel class
models.py
class IncompletedLecture(Lecture):
class Meta:
proxy = True
admin.py
class IncompletedAdmin(admin.ModelAdmin):
def get_queryset():
return Lecture.query.exclude(Status='Completed')
admin.site.register(IncompletedLecture, IncompletedAdmin)

Categories