Caching reverse foreign key object in django admin display - foreign-keys

I'm trying to fetch some different attribute from reverse foreign key object and show it on django admin list_display. But this current method will call the db queries multiple times
models.py:
class Author(models.Model):
name = models.CharField()
...
def get_all_book(self):
return self.book_set.all()
class Book(models.Model):
author = models.ForeignKey(Author)
aaa = some field type
bbb = some field type
...
admin.py:
class AuthorAdmin(admin.ModelAdmin):
def book_aaa(obj):
booklist = obj.get_all_book
all_bookaaa = ",".join([k.aaa for k in booklist])
return all_bookaaa
def book_bbb(obj):
booklist = obj.get_all_book
all_bookbbb = ",".join([k.bbb for k in booklist])
return all_bookbbb
list_display = ('name', book_aaa, book_bbb)
admin.site.register(Author, AuthorAdmin)
Because I need to separate those book information in separate column, but if using this method, it might called "book_set.all()" queryset twice, which is very bad for the performance. Is there any correct method to implement this problem?

By creating extra attribute to the object, and check whether the attribute exists or not.
def get_booklist(self, obj):
if not hasattr(obj, 'booklist')
obj.booklist = obj.get_all_book
return obj
def book_aaa(self, obj):
booklist = self.get_booklist(obj).booklist
all_bookaaa = ",".join([k.aaa for k in booklist])
return all_bookaaa
def book_bbb(self, obj):
booklist = self.get_booklist(obj).booklist
all_bookbbb = ",".join([k.bbb for k in booklist])
return all_bookbbb
list_display = ('name', 'book_aaa', 'book_bbb')
Maybe this is not the best solution, but at least can prevent the queryset called multiple times.

Related

django admin sort calculated fields in listview

I have two models, one (Device) which has a foreign key to the other (Storage). In the admin overview of Storage I wanna be able to add a count of how many Device has pointed to each Storage. Using the code below I'm able to correctly count the number of Devices pointing to each Storage. However, when trying to sort by the number in storage it crashes with the following error:
Cannot resolve keyword '_get_device_number' into field. Choices are:
... #List of fields
How do I add my calculated field into the list allowed searches?
class Device(models.Model):
...
storage_id = models.ForeignKey(Storage, on_delete=models.PROTECT, blank=True,null=True) #Allowed to be null since a Device may not be in storage.
class Storage(models.Model):
...
def _get_device_number(self,):
return Device.objects.filter(storage_id=self.storage_id).count()
class StorageAdmin(admin.ModelAdmin):
...
list_display = ['__str__', 'get_device_number',]
def get_device_number(self, obj):
return obj._get_device_number()
get_device_number.admin_order_field = '_get_device_number'
The admin list view cannot access a model method. However, annotating the admin queryset does work.
class StorageAdmin(admin.ModelAdmin):
def get_queryset(self, request):
queryset = super().get_queryset(request)
queryset = queryset.annotate(
_get_device_number=Count('device')
)
return queryset
def get_device_number(self, obj):
return obj._get_device_number
get_device_number.admin_order_field = '_get_device_number'

Django Rest Framework categories and childs in one model

I have a very simple ( with a first look) problem. Case - A product can be sold in a several places(shops), and every product can be represented in a single shop with a different categories and sub categories ( That is why category linked via ForeignKey with Assortment twice).
So here is My Assortment model, with several FKs.
class Assortment(models.Model):
category = models.ForeignKey('category.Category', null=True, blank=True, default=None,related_name='assortment_child')
parent_category = models.ForeignKey('category.Category', null=True, blank=True, default=None,related_name='assortment_parent')
product = models.ForeignKey(Product)
shop = models.ForeignKey(Shop)
View, based on rest_framework.generics.ListAPIView
class InstitutionTreeCategories(generics.ListAPIView):
"""Resource to get shop's tree of categories."""
serializer_class = serializers.InstitutionCategoriesSerializer
def get_queryset(self):
shop = self.get_shop()
return Category.objects.filter(assortment_parent__shop=shop).distinct()
And finally, serializers
class CategoryListSerializer(serializers.ModelSerializer):
class Meta:
"""Meta class."""
model = Category
fields = ('id', 'name', 'image')
class CategoriesTreeSerializer(CategoryListSerializer):
# childs = CategoryListSerializer(many=True, source='assortment_child__parent_category')
childs = serializers.SerializerMethodField()
class Meta(CategoryListSerializer.Meta):
"""Meta class."""
fields = ('id', 'name', 'image', 'childs')
def get_childs(self, obj):
qs = Category.objects.filter(assortment_child__parent_category=obj.id).distinct()
return CategoryListSerializer(qs, many=True, context=self.context).data
And i need to show Category Tree for a one single shop with my API.
But the problem is - If I use serializer.SerializerMethodField - it works, but too many queries (for every parent category). I tried to avoid it using 'source' option with my 'CategoryListSerializer' by I can't make it. Every time, I get - 'Category' object has no attribute assortment_child__parent_category. In a shell model i've tried
In [8]: cat.assortment_parent.values('category').distinct()
Out[8]: (0.003) SELECT DISTINCT "marketplace_assortment"."category_id" FROM "marketplace_assortment" WHERE "marketplace_assortment"."parent_category_id" = 4 LIMIT 21; args=(4,)
<AssortmentQuerySet [{'category': 3}]>
So - category object has this attributes, of course it does, i used it a get_childs method. So question is - how i can use it with serializer.ModelSerializer and it's source option? ( Of course using select_related method with queryset, to avoid excess queries).
by source option you should use . in instead of __:
childs = CategoryListSerializer(many=True, source='assortment_child.parent_category')
but still you will has many queries, to fix it you should use prefetch-related
def get_queryset(self):
shop = self.get_shop()
qs = Category.objects.filter(assortment_parent__shop=shop).all()
return qs.prefetch_related('assortment_child').distinct()
more detail you can read in the how-can-i-optimize-queries-django-rest-framework
I had the similar problem and the best solution I have found is to do some manual processing in order to receive desired tree representation.
So firstly we fetch all Assortment for shop and then build the tree manually.
Let's look at the example.
def get_categories_tree(assortments, context):
assortments = assortments.select_related('category', 'parent_category')
parent_categories_dict = OrderedDict()
for assortment in assortments:
parent = assortment.parent_category
# Each parent category will appear in parent_categories_dict only once
# and it will accumulate list of child categories
if parent not in parent_categories_dict:
parent_data = CategoryListSerializer(instance=parent, context=context).data
parent_categories_dict[parent] = parent_data
parent_categories_dict[parent]['childs'] = []
child = assortment.category
child_data = CategoryListSerializer(instance=child, context=context).data
parent_categories_dict[parent]['childs'].append(child_data)
# convert to list as we don't need the keys already - they were used only for matching
parent_categories_list = list(parent_categories_dict.values())
return parent_categories_list
class InstitutionTreeCategories(generics.ListAPIView):
def list(self, request, *args, **kwargs):
shop = self.get_shop()
assortments = Assortment.objects.filter(shop=shop)
context = self.get_serializer_context()
categories_tree = get_categories_tree(assortments, context)
return Response(categories_tree)
All in single DB query.
The problem here is that there is no explicit relation between category and parent_category. If you define ManyToManyField in Category using Assortment as through intermediate model, you will get an access which Django can understand, so you would just use attribute childs on Category for example. However this will still return all children (the same would happen if your source example works) categories, ignoring shop, so some clever Prefetch would have to be done to achieve correct results. But I believe manual "join" is simpler.
you need to use prefetch_related along with serializer method field
serializer:
class CategoriesTreeSerializer(CategoryListSerializer):
children = serializers.SerializerMethodField()
class Meta(CategoryListSerializer.Meta):
fields = (
'id',
'name',
'image',
'children'
)
def get_children(self, obj):
children = set()
for assortment in obj.assortment_parent.all():
children.add(assortment.category)
serializer = CategoryListSerializer(list(children), many=True)
return serializer.data
your get queryset method:
def get_queryset(self):
shop = self.get_shop()
return (Category.objects.filter(assortment_parent__shop=shop)
.prefetch_related(Prefetch('assortment_parent', queryset=Assortment.objects.all().select_related('category')
.distinct())

Django Beginner. How do I update all objects and set a certain field to a value that is a function of another field?

EDIT:
I needed a student_count field in course because I'm supposed to use this model for REST API. If you can tell me how to add fields in serializer without adding to model, I'd take that too.
This is my model:
class Course(models.Model):
student_count = models.PositiveIntegerField(default=0)
students = models.ManyToManyField()
What I try to do is the following but it doesn't work.
Course.objects.update(student_count=F('students__count'))
The following works but it's not ideal
courses = Course.objects.all()
for course in courses:
course.student_count = course.students.count()
course.save()
return Course.objects.all()
Try to add/update your serializer as below,
from django.db.models import Count
class CourseSerializer(serializers.ModelSerializer):
count = serializers.SerializerMethodField(read_only=True)
def get_count(self, model):
return model.students.aggregate(count=Count('id'))['count']
class Meta:
model = Course
fields = ('count',)
You can use aggregation Count() method instead of queryset.count() its more faster. Read this SO post, aggregate(Count()) vs queryset.count()

Django Rest Framework: How to save a field as an object of a different model, then return the key to the original model

Sorry for the weird title.
I have 2 models:
class TranslationWord(models.Model):
translation = models.TextField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class TranslationByUser(models.Model):
synset = models.ForeignKey(Synset)
user = models.ForeignKey(User)
translation = models.ForeignKey(TranslationWord)
The first one is supposed to basically just save words. The second is supposed to get a user's input. If the word exists in the first class, the foreign key value is simply stored. If it doesn't exist, I want to first create an instance of the TranslationWord, and then add the foreign key to the second class.
I'm doing all this with the Django Rest Framework, so I'm pretty stumped.
Currently, I've got these 2 models, 2 serializers (both just instances of ModelSerializer), and a view to save it (ListCreateAPIView).
How should I go about doing this?
These are basically the steps for creating a successfully validated object in a ModelViewSet create method (it's defined in CreateModelMixin):
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
It means you can override pre_save for your action in your ViewSet for TranslationByUser, setting attributes as side-effects in the object:
def pre_save(self, obj):
#get your_translation_word from obj or self.kwargs
your_translation_word = get_translation_word()
translation = TranslationWord(translation=your_translation_word)
translation.save()
setattr(obj, 'translation', translation)
#if you also want to support Update, call super method
super(TranslationByUserViewSet, self).pre_save(obj)
Another thing you can try is to define TranslationWordSerializer as a nested field in TranslationByUserSerializer. This topic is explained in the docs. Not sure if DRF handles everything about the creation though. I've only tested this behaviour with Multi-table Inheritance (and it works).
Anyway, for anyone who is curious, I created a write-only field in the Serializer, and used it to create the instance in the restore_object method.
class MySerializer(serializers.ModelSerializer):
user = UserSerializer(required=False)
translation = TranslationLemmaSerializer(required=False)
translation_text = serializers.WritableField(required=False, write_only=True)
class Meta:
model = TranslationByUser
fields = ('id','user','synset', 'translation', 'translation_text',)
read_only_fields = ('id', 'created_at', 'updated_at',)
def restore_object(self, attrs, instance=None):
print attrs
if instance is not None:
instance.synset = attrs.get('synset', instance.synset)
return instance
translation_text = attrs.get('translation_text')
del attrs['translation_text'] #delete non-model attribute before actually creating it
translationHIT = TranslationByUser(**attrs) #create model here
translation_id = None
try:
translation_instance = TranslationWord.objects.get(translation=translation_text) #check if translationWord is already present
except:
translation_instance = TranslationWord(translation=translation_text)
translation_instance.save() #otherwise, create it
TranslationByUser.translation = translation_instance
print attrs
return TranslationByUser
def get_validation_exclusions(self,instance=None):
exclusions = super(MySerializer, self).get_validation_exclusions()
return exclusions + ['user', 'translation']

field choices() as queryset?

I need to make a form, which have 1 select and 1 text input. Select must be taken from database.
model looks like this:
class Province(models.Model):
name = models.CharField(max_length=30)
slug = models.SlugField(max_length=30)
def __unicode__(self):
return self.name
It's rows to this are added only by admin, but all users can see it in forms.
I want to make a ModelForm from that. I made something like this:
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=CHOICES),
}
but it doesn't work. The select tag is not displayed in html. What did I wrong?
UPDATE:
This solution works as I wanto it to work:
class ProvinceForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProvinceForm, self).__init__(*args, **kwargs)
user_provinces = UserProvince.objects.select_related().filter(user__exact=self.instance.id).values_list('province')
self.fields['name'].queryset = Province.objects.exclude(id__in=user_provinces).only('id', 'name')
name = forms.ModelChoiceField(queryset=None, empty_label=None)
class Meta:
model = Province
fields = ('name',)
Read Maersu's answer for the method that just "works".
If you want to customize, know that choices takes a list of tuples, ie (('val','display_val'), (...), ...)
Choices doc:
An iterable (e.g., a list or tuple) of
2-tuples to use as choices for this
field.
from django.forms.widgets import Select
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=( (x.id, x.name) for x in CHOICES )),
}
ModelForm covers all your needs (Also check the Conversion List)
Model:
class UserProvince(models.Model):
user = models.ForeignKey(User)
province = models.ForeignKey(Province)
Form:
class ProvinceForm(ModelForm):
class Meta:
model = UserProvince
fields = ('province',)
View:
if request.POST:
form = ProvinceForm(request.POST)
if form.is_valid():
obj = form.save(commit=True)
obj.user = request.user
obj.save()
else:
form = ProvinceForm()
If you need to use a query for your choices then you'll need to overwrite the __init__ method of your form.
Your first guess would probably be to save it as a variable before your list of fields but you shouldn't do that since you want your queries to be updated every time the form is accessed. You see, once you run the server the choices are generated and won't change until your next server restart. This means your query will be executed only once and forever hold your peace.
# Don't do this
class MyForm(forms.Form):
# Making the query
MYQUERY = User.objects.values_list('id', 'last_name')
myfield = forms.ChoiceField(choices=(*MYQUERY,))
class Meta:
fields = ('myfield',)
The solution here is to make use of the __init__ method which is called on every form load. This way the result of your query will always be updated.
# Do this instead
class MyForm(forms.Form):
class Meta:
fields = ('myfield',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make the query here
MYQUERY = User.objects.values_list('id', 'last_name')
self.fields['myfield'] = forms.ChoiceField(choices=(*MYQUERY,))
Querying your database can be heavy if you have a lot of users so in the future I suggest some caching might be useful.
the two solutions given by maersu and Yuji 'Tomita' Tomita perfectly works, but there are cases when one cannot use ModelForm (django3 link), ie the form needs sources from several models / is a subclass of a ModelForm class and one want to add an extra field with choices from another model, etc.
ChoiceField is to my point of view a more generic way to answer the need.
The example below provides two choice fields from two models and a blank choice for each :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=([['','-'*10]]+[[x.id, x.__str__()] for x in Speakers.objects.all()]))
event = forms.ChoiceField(choices=( [['','-'*10]]+[[x.id, x.__str__()] for x in Events.objects.all()]))
If one does not need a blank field, or one does not need to use a function for the choice label but the model fields or a property it can be a bit more elegant, as eugene suggested :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=((x.id, x.__str__()) for x in Speakers.objects.all()))
event = forms.ChoiceField(choices=(Events.objects.values_list('id', 'name')))
using values_list() and a blank field :
event = forms.ChoiceField(choices=([['','-------------']] + list(Events.objects.values_list('id', 'name'))))
as a subclass of a ModelForm, using the one of the robos85 question :
class MixedForm(ProvinceForm):
speaker = ...