Django queryset annotate calculated values from another model - django

I have 4 models in a hierarchy: analysis, books, category and publisher. In my app you analyze books. The books belong to categories where you can view the average analysis rating of all the books in the category. This average rating (that I call avg-catg-rating) is not stored in the dB, it’s calculated in the view.
Here’s my question: how do I get the average category rating for each category that the publisher has onto a single publisher view? In other words, how do I annotate the avg-catg-rating onto each category so I can display all of them on the publisher’s page?
So do I iterate using the category somehow like this question? If yes, please give a tip on how because I'm not really sure how to go about it. I also tried groupby as suggested by this chap, but I could only get the count of book instances, I couldn't accurately annotate the avg-catg-rating.
To clarify:
Models.py:
class Analysis:
title = models.CharField
content_rating_1 = models.IntegerField(blank=True,
null=True, default="0")
content_rating_1_comment = models.TextField(max_length=300, blank=True,
null=True)
source_rating_1 = models.IntegerField(blank=True,
null=True, default="0")
source_rating_1_comment = models.TextField(max_length=300, blank=True,
null=True)
book = models.ForeignKey
class Book:
publication_date = models.DateField(default=datetime.date.today)
slug = models.SlugField(allow_unicode=True, unique=False, max_length=160)
category = models.ForeignKey
class Category:
title = models.CharField(max_length=150, unique=False, blank=False)
sub_title = models.CharField(max_length=100, blank=True, null=True)
publisher = models.ForeignKey
class Publisher:
title = models.CharField(max_length=150, unique=False, blank=False)
publisher/views.py:
class ViewPublisher:
def get_context_data(self, **kwargs):
category_qs = Category.objects.filter(publisher__pk=self.kwargs['pk'])
avg-catg-rating = Books.objects.annotate(long complex queryset that uses values from the analysis model)
context = super(ViewPublisher, self).get_context_data(**kwargs)
context['categories'] = category_qs
context['category_stats'] = Books.objects.filter(category__pk__in=category_qs)\
.annotate(.....and now what???)

I let go of my obsession with doing everything in the view.
The solution is to create a method in your models that you can call in the template. In my case, I created a method in the Category/models.py like so:
class Category:
title = models.CharField(max_length=150, unique=False, blank=False)
sub_title = models.CharField(max_length=100, blank=True, null=True)
publisher = models.ForeignKey
def sum_catg_avg_rating(self):
scar = self.books.annotate(avg-catg-rating=(annotate the AVG calculations from the
analysis))
return scar
Then included the category into the context of the publisher:
class ViewPublisher:
def get_context_data(self, **kwargs):
category_qs = Category.objects.filter(publisher__pk=self.kwargs['pk'])
context = super(ViewPublisher, self).get_context_data(**kwargs)
context['categories'] = category_qs
return context
Now, I can just call it in the template:
{% for catg in categories %}
<h5>{{ catg.title }}</h5>
<p> RATING:<i>{{ catg.sum_catg_avg_rating.avg-catg-rating }}</i></p>

Related

Django - ForeignKey Filter Choices

I'd like to filter the choices that a user can choose in my ForeignKey Field.
I basically have a ForeignKey for the subject of the Test and the actual topic of the Test. These topics come from a different model and are linked to a subject. Now I'd like to filter the choices to only include the topics that are linked to the currently selected subject. Is that possible and if so, how?
models.py
class Test(models.Model):
student = models.ForeignKey(Person, on_delete=models.CASCADE, blank=True, null=True)
subject = models.ForeignKey(Subject, on_delete=models.CASCADE, blank=True, null=True)
thema = models.ForeignKey(Thema, on_delete=models.CASCADE, blank=True, null=True)
school_class = models.ForeignKey(SchoolClass, on_delete=models.CASCADE, blank=True, null=True)
grade = models.FloatField(validators=[MinValueValidator(0), MaxValueValidator(6)], blank=True, null=True)
date = models.DateField(default=datetime.date.today)
def save(self, *args, **kwargs):
if not self.school_class and self.student:
self.school_class = self.student.klasse
return super().save(*args, **kwargs)
class Thema(models.Model):
subject = models.ForeignKey(Subject, on_delete=models.CASCADE, blank=True, null=True)
thema = models.CharField(max_length=50)
class Subject(models.Model):
teacher = models.ForeignKey(Person, on_delete=models.CASCADE, blank=True, null=True)
name = models.CharField(max_length=20)
The Problem if I use this:
# thema model #staticmethod
def return_thema(subject):
themen = Thema.objects.filter(subject=subject)
return {'thema': themen}
#test model
thema = models.ForeignKey(Thema, on_delete=models.CASCADE, blank=True, null=True,limit_choices_to=Thema.return_thema(subject))
Is that I get the Error:
django.core.exceptions.AppRegistryNotReady: Models aren't loaded yet.
Meaning I can't get the objects of the Thema Model while the models are loading
EDIT (for Swift):
That seemed to resolve the error when trying to makemigrations, but I now get this error, when visiting the admin portal to create a new Test:
File "/Users/di/Code/Schule/GymnasiumApp/venv/lib/python3.10/site-packages/django/db/models/sql/query.py", line 1404, in build_filter
arg, value = filter_expr
ValueError: too many values to unpack (expected 2)
I think what you are looking for ideally would be ForeignKey.limit_choices_to
Please see the docs:
https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.ForeignKey.limit_choices_to
You can limit the choices available at a model level, which is enforced throughout the django app, including forms automatically.
Edit because OP provided more information
Ok so I believe if you declare the thema field on the test model like so, it will solve the issue, and I will explain why after:
class Test(models.Model):
student = models.ForeignKey(Person, on_delete=models.CASCADE, blank=True, null=True)
subject = models.ForeignKey(Subject, on_delete=models.CASCADE, blank=True, null=True)
thema = models.ForeignKey(Thema, on_delete=models.CASCADE, blank=True, null=True, limit_choices_to=Q('thema_set__subject_set'))
school_class = models.ForeignKey(SchoolClass, on_delete=models.CASCADE, blank=True, null=True)
grade = models.FloatField(validators=[MinValueValidator(0), MaxValueValidator(6)], blank=True, null=True)
date = models.DateField(default=datetime.date.today)
def save(self, *args, **kwargs):
if not self.school_class and self.student:
self.school_class = self.student.klasse
return super().save(*args, **kwargs)
We are essentially telling Django to evaluate the relationship between the limited choices "lazily" I.e. when the form is loaded dynamically. Django forms will look at the limit_choices_to field argument and apply it to the available choices.
I'm not 100% about the relationship of your models so the Q(...) I added, might actually need to be Q('subject_set')
If you use django forms you can use the model choice field.
In your view you can set your queryset of this choicefield. Zo you can filter it.
fields['your model field'].queryset = yourmodel.objects.filter(your filter parameters)
I think there is also problem in save method also. Aren't you need to write the name of the model inside like
return super(<modelName>).save(*args, **kwargs)

Filter queryset of django inline formset based on attribute of through model

I have a basic restaurant inventory tracking app that allows the user to create ingredients, menus, and items on the menus. For each item on a given menu, the user can list the required ingredients for that item along with a quantity required per ingredient for that item.
Menu items have a many-to-many relationship with ingredients, and are connected via an "IngredientQuantity" through table.
Here are my models:
class Ingredient(models.Model):
GRAM = 'Grams'
OUNCE = 'Ounces'
PIECE = 'Pieces'
UNIT_CHOICES = [
('Grams', 'Grams'),
('Ounces', 'Ounces'),
('Pieces', 'Pieces')
]
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
name = models.CharField(max_length=200)
unitType = models.CharField(max_length=200, choices=UNIT_CHOICES, verbose_name='Unit')
unitCost = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Unit Cost')
inventoryQuantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Quantity')
def __str__(self):
return self.name + ' (' + self.unitType + ')'
def totalCost(self):
result = self.inventoryQuantity * self.unitCost
return "{:.0f}".format(result)
class Menu(models.Model):
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
name = models.CharField(max_length=200)
timeCreated = models.DateTimeField(auto_now_add=True)
timeUpdated = models.DateTimeField(auto_now=True)
def __str__(self):
return str(self.name)
class MenuItem(models.Model):
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
ingredients = models.ManyToManyField(Ingredient, through='IngredientQuantity')
menu = models.ForeignKey(Menu, on_delete=models.CASCADE, null=True)
def __str__(self):
return self.name
def itemCost(self):
relevantIngredients = IngredientQuantity.objects.filter(menuItem=self)
cost = 0
for ingredient in relevantIngredients:
cost += (ingredient.ingredient.unitCost * ingredient.ingredientQuantity)
return cost
class IngredientQuantity(models.Model):
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE)
menuItem = models.ForeignKey(MenuItem, on_delete=models.CASCADE)
ingredientQuantity = models.IntegerField(default=0)
def __str__(self):
return str(self.ingredient)
This is a multi-user app, so when a user creates a new item on a menu and adds ingredients to it, they should only have the option of choosing ingredients they have created, not those of other users. Here is my attempt to do that in my views:
def ItemUpdate(request, pk):
item = MenuItem.objects.get(id=pk)
user = request.user
IngredientQuantityFormset = inlineformset_factory(
MenuItem, IngredientQuantity, fields=('ingredient', 'ingredientQuantity'), can_delete=True, extra=0
)
form = ItemCreateForm(instance=item)
formset = IngredientQuantityFormset(instance=item, queryset=IngredientQuantity.objects.filter(ingredient__user=user))
if request.method == 'POST':
form = ItemCreateForm(request.POST, instance=item)
formset = IngredientQuantityFormset(request.POST, instance=item, queryset=IngredientQuantity.objects.filter(ingredient__user=user))
# rest of view...
I've searched everywhere for how to implement the queryset parameter properly, but I cannot get it to work. When creating an item on a menu, the user still has the ability to choose from every ingredient in the database (including the ones created by other users). I would like the user to only be able to choose from the ingredients they themselves created.
Does anyone know how to do this properly? Thank you!
I received some guidance on Django forums and arrived at a solution which is documented below:
https://forum.djangoproject.com/t/filter-dropdown-options-in-django-inline-formset-based-on-attribute-of-through-model/13374/3

Django View that shows related model context

I'm learning and I'm struggling to create a Django view that displays a Company Detail model, but also can show the associated location names that are located in child FK models to the Company.
What I've tried includes these models:
class Company(models.Model):
company_name = models.CharField(max_length=100, unique=True)
street = models.CharField('Street', max_length=100, null=True, blank=True)
city = models.CharField('City', max_length=100, null=True, blank=True)
province = models.CharField('Province/State', max_length=100, null=True, blank=True)
code = models.CharField('Postal/Zip Code', max_length=100, null=True, blank=True)
company_comments = models.TextField('Comments', max_length=350, null=True, blank=True)
class Region(models.Model):
company = models.ForeignKey(Company, on_delete=models.CASCADE)
region_name = models.CharField(max_length=100)
description = models.TextField(blank=True)
def __str__(self):
return self.region_name
class District(models.Model):
region = models.ForeignKey(Region, on_delete=models.CASCADE)
district_name = models.CharField(max_length=100)
description = models.TextField(blank=True)
dictrict_ops_leadership = models.CharField(max_length=100, blank=True)
def __str__(self):
return self.district_name
My view looks like:
class CompanyDetailView(DetailView):
model = Company
template_name = 'main/company_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['region'] = Region.objects.all()
context['district'] = District.objects.all()
return context
I want to call **all instances of Region and District** for the **single instance of Company.**
With my HTML tags as : <p>{{ region }}{{ district }}</p>
It just returns a list of query's of all Region and District instances:
<QuerySet [<Region: RegionA>, <Region: RegionB>]>
<QuerySet [<District: DistrictA>, <District: DistrictB>]>
Any help would be GREATLY appreciated.
Thanks
Region.objects.filter(company__company_name = 'Your Company Name')
District.objects.filter(company__company_name = 'Your Company Name')
This will give You to the Region and District they related to the Your Comapany Name

Django: set model choice field options to queryset values and other value

In the below Django models
class Post(models.Model):
author = models.ForeignKey(CustomUser, on_delete=models.CASCADE,)
title = models.CharField(max_length=200, null=True)
text = models.TextField()
post_url = models.URLField(max_length = 200, blank=True)
post_type = models.IntegerField()
slug = models.SlugField(unique=True, blank=True)
class Tiers(models.Model):
user = models.ForeignKey(CustomUser, default=None, null=True, on_delete=models.CASCADE,)
tier_name = models.CharField(max_length=128, blank=True)
tier_value = models.IntegerField()
slug = models.SlugField(unique=True, blank=True)
I want to use post model for a form like below
class PostForm(forms.ModelForm):
class Meta:
model = models.Post
fields = ('title', 'text', 'post_url', 'post_type')
But for post_type field I want to display as dropdown with options from Tiers models tier_value. For example if user1 has 3 entries in Tiers model with tier_values as 10, 20 and 30. I want to display 4 options 0, 10, 20 , and 30. Can someone help me how to achieve this?
In theory, you can override the FormField django generates. In this case, you could use a Select widget for the post_type field.
However, I think with your modeling can be improved/normalized, and if that is done, your issue will resolve. Consider these models:
class Tier(models.Model):
name = models.CharField(max_length=128, blank=True)
value = models.IntegerField()
slug = models.SlugField(unique=True, blank=True)
class CustomUser(...):
tiers = models.ManyToManyField(Tier, related_name='users')
...
class Post(models.Model):
author = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
tier = models.ForeignKeyField(Tier)
title = models.CharField(max_length=200, null=True)
text = models.TextField()
url = models.URLField(max_length = 200, blank=True)
slug = models.SlugField(unique=True, blank=True)
This way, you different users can have the same tier (which is probably what you want?) without duplication of the name and level values.
Now, when you create ModelForm with Post, it will automatically give you select field for all existing tiers. However, you just want to be able to select the tiers the user is in, so you would set a custom queryset for that field:
class PostForm(forms.ModelForm):
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['tier'].queryset = Tier.objects.filter(users__in=[user])
class Meta:
model = models.Post
fields = ('title', 'text', 'url', 'tier')
Edit: I just now saw that you want to allow all tiers that the user is in or that have less value. You can do this the same way, you just have to adapt the queryset:
max_tier_value = user.tiers.aggregate(Max('value')).value__max
self.fields['tier'].queryset = Tier.objects.filter(value__lte=max_tier_value)
However, you probably want to do either of these two, but not both:
Each user is assigned with a tier level and can create posts with any lower tier level.
Each user is assigned with multiple tier levels and can only create posts with these.
So, if you go with this queryset, you should remodel so that CustomUser.tier is a models.ForeignKey(Tier, related_name='users')

django queryset - need advice on queryset in view - filters and setting relationships

I am trying to generate a template/report that lists all of the listings (ads) for a customer. Most everything relates to the ID of the Listings table. A customer can have many listings, a listing will only have one listing type, and a listing can have many images. I know my view.py is messed up -- ideally I'd like to send a minimal amount of data to the template. So I would like to only send listings, images and listingtype (1, 2, or 3) data that relates to listings for a specific customer. I'm struggling with the queryset, and building the context. I'm sure I have to add more objects to the context than are currently listed.
I'm am presuming once I get the data to the template I will have to build a table row by row, and do some if/then stuff in the template to deal with the different listingtypes. Let me know if you know of an easier way.
Models.py
class Customer(models.Model):
name = models.CharField(max_length=20, blank=True)
email = models.CharField(max_length=50, blank=True)
user = models.ForeignKey(User, unique=True)
def __unicode__(self):
return unicode(self.user)
class ListingType(models.Model):
desc = models.CharField(max_length=35, blank=True)
def __unicode__(self):
return self.desc
class Listings(models.Model):
createdate = models.DateTimeField(auto_now_add=True)
price = models.IntegerField(null=True, blank=True)
listing_type = models.ForeignKey(ListingType)
customer = models.ForeignKey(Customer)
class Listingtype1(models.Model):
manufacturer = models.CharField(max_length=35, blank=True)
mfg_no = models.CharField(max_length=35, blank=True)
typespecific1 = models.CharField(max_length=35, blank=True)
typespecific2 = models.CharField(max_length=35, blank=True)
listings = models.ForeignKey(Listings)
class Listingtype2(models.Model):
manufacturer = models.CharField(max_length=35, blank=True)
mfg_no = models.CharField(max_length=35, blank=True)
typespecific1 = models.CharField(max_length=35, blank=True)
typespecific2 = models.CharField(max_length=35, blank=True)
listings = models.ForeignKey(Listings)
class Listingtype3(models.Model):
manufacturer = models.CharField(max_length=35, blank=True)
mfg_no = models.CharField(max_length=35, blank=True)
typespecific1 = models.CharField(max_length=35, blank=True)
typespecific2 = models.CharField(max_length=35, blank=True)
listings = models.ForeignKey(Listings)
class Image(models.Model):
title = models.CharField(max_length=60, blank=True, null=True)
image = models.ImageField(upload_to="images/", blank=True, null=True)
thumbnail = models.ImageField(upload_to="images/", blank=True, null=True)
listings = models.ForeignKey(Listings)
Views.py (work in progress)
def listings_customer(request, user_id):
customer = get_object_or_404(Customer, user=user_id)
cusnum=customer.id
listings = Listings.objects.filter(customer=cusnum)
image = Image.objects.all()
context=Context({
'title': 'Listings',
'customer': customer,
'listings' : listings,
'image' : image,
})
return render_to_response('bsmain/listings.html', context)
Take a look on lookups with relations and backwards relationship so you can link all your models in chains like:
Image.objects.filter(listings__customer=customer)
Also, few offtopic advices.
a listing will only have one listing type
So you should use OneToOneField here
You don't need to retrieve customer.id, use customer for lookup.
Use non-plural names for your models (Listings should be Listing) and avoid duplicating your code like in ListingType1 and ListingType2, use model inheritance