I'm pretty new to search implementation, bear with me while I'm learning!
So my pet project is a recipe site and each recipe can have n steps. the model looks something like:
class Recipe(models.Model):
title = models.CharField(max_length=255)
description = models.TextField()
hotness = models.ForeignKey(Hotness)
recipe_diet = models.ManyToManyField(DietType)
ingredients = models.ManyToManyField(Ingredient, through="RecipeIngredient")
class DietType(models.Model):
diet_type = models.CharField(max_length=50)
description = models.TextField(null=True, blank=True)
class RecipeIngredient(models.Model):
recipe = models.ForeignKey(Recipe)
ingredient = models.ForeignKey(Ingredient)
quantifier = models.ForeignKey(Quantifier)
quantity = models.FloatField()
class RecipeSteps(models.Model):
step_number = models.IntegerField()
description = models.TextField()
recipe = models.ForeignKey(Recipe)
(shortened for brevity)
I want to index all of it: Recipe, RecipeIngredient, DietType and Steps...
The DietType and RecipeIngredient seem to be working fine, but the Steps are not. I assume this has to do with the usage of 'RelatedSearchQuerySet' ?
Here is my search_indexes.py:
from haystack import indexes
from recipes.models import Recipe
class RecipeIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
title = indexes.CharField(model_attr='title')
ingredients = indexes.MultiValueField(indexed=True, stored=True)
description = indexes.CharField(model_attr='description')
hotness = indexes.CharField(model_attr='hotness')
diet_type = indexes.MultiValueField(indexed=True, stored=True)
recipesteps = indexes.MultiValueField(indexed=True, stored=True)
def prepare_steps(self, object):
return [step.description for step in object.recipesteps.all()]
def get_model(self):
return Recipe
def load_all_queryset(self):
# Pull all objects related to the Note in search results.
return Recipe.objects.all().select_related()
Here is the template recipe_text.txt:
{{ object.title }}
{{ object.cuisine }}
{% for ingr in object.ingredients.all %}
{{ ingr.title }}
{% endfor %}
{{ object.description }}
{% for dt in object.recipe_diet.all %}
{{ dt.diet_type }}
{% endfor %}
{{ object.user }}
{{ object.hotness }}
{% for step in object.recipesteps.all %}
{{ step.description }}
{% endfor %}
{{ object.body }}
I can search ingredients, title, description, diet type - everything works, except the RecipeSteps.
Finally, I'm making queries through the shell only at the moment:
#producing results:
sq = SearchQuerySet().filter(content='onion') #ingredient
sq = SearchQuerySet().filter(content='bolognese') #title
sq = SearchQuerySet().filter(content='bologna') #description
#not producing any results:
sq = SearchQuerySet().filter(content='chop') #step
sq = RelatedSearchQuerySet().filter(content='chop').load_all() #assuming this does the expanded search
Any idea?
I have identified two issues:
The name prepare_steps in your RecipeIndex is wrong it should be prepare_{field_name} so change it to prepare_recipesteps
You are trying to access related steps object.recipesteps.all objects recipe_text.txt in a wrong way, it should be object.recipesteps_set.all. Or keep using recipesteps but add this as a related_name in RecipeSteps model for ForeignKey Recipe e.g.
class RecipeSteps(models.Model):
# //
recipe = models.ForeignKey(Recipe, related_name='recipesteps')
Related
During work on my 1st app(kind of cookery book where it will be possible also to create meal plans) i have a problem to addapt one field from many-to-many(through) model to my html template. Field name is 'meal' in RecipeMealPlan model.
Here are my models:
class Recipe(models.Model):
title = models.CharField(max_length=50)
cooking_time = models.IntegerField(help_text='in minutes', validators=[MinValueValidator(1), MaxValueValidator(5000)])
difficulty_level = models.IntegerField(choices=DIFFICULTY_LEVELS, default=1)
description = models.TextField()
created = models.DateTimeField(auto_now_add=True)
cuisine = models.ForeignKey('Cuisine', on_delete=models.CASCADE, null=True)
ingredient = models.ManyToManyField(Ingredient, through='IngredientRecipe')
meal_plan = models.ManyToManyField('MealPlan', through='RecipeMealPlan')
class RecipeMealPlan(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
meal_plan = models.ForeignKey('MealPlan', on_delete=models.CASCADE)
meal = models.IntegerField(choices=MEALS)
MEALS = (
(1, 'Breakfast'),
(2, '2nd breakfast'),
(3, 'Lunch'),
(4, 'Snack'),
(5, 'Dinner')
)
class MealPlan(models.Model):
name = models.CharField(max_length=50)
amount = models.IntegerField(validators=[MinValueValidator(4), MaxValueValidator(6)])
Here is my view created to show mealplan details on my app:
class MealPlanDetailsView(View):
def get(self, request, id):
mealplan = MealPlan.objects.get(id=id)
recipes = mealplan.recipe_set.all()
return render(request, 'diet_app/mealplan_details.html', {'mealplan': mealplan, 'recipes': recipes})
And html template:
{% extends 'diet_app/base.html' %}
{% block title %}{{ mealplan|upper }}{% endblock %}
{% block content %}
<h2>{{ mealplan|upper }}</h2>
<ul> <p>Posiłki:</p>
{% for recipe in mealplan.recipemealplan_set.all %}
<li>{{ recipe.get_meal_display}}: {{ recipe }}</li>
{% endfor %}
</ul>
{% endblock %}
Everything looks fine but link to receipe details doestnt work:
<a href="/recipe/{{recipe.id}}/">
Link works if i write the loop like this:
{% for recipe in recipes %}
<li>{{ recipe.title }} </li>
{% endfor %}
But then i dont see meal name before recipe (meal name means Breakfast, dinner etc.). I don't how to write it down to see together meal name and recipe with link to recipe details.
I succeed only when i wrote those 2 loops combined but then i see my meal plan repeated few times.
Any ideas what should i do to make it work the way i want?
recipe.id is the id of the through model RecipeMealPlan, and not Recipe, so instead of recipe.id, you need to use recipe.recipe.id.
Also for sanity's sake, you could use something like recipemealplan instead of recipe as the variable name, so:
{% for recipemealplan in mealplan.recipemealplan_set.all %}
<li>{{ recipemealplan.get_meal_display}}: {{ recipemealplan }}</li>
{% endfor %}
For each Product I need to show all it's reviews with an attached profilepicture, this requires queries into 3 models but I need a maximum of 2 for loops as when I do the Profile for loop all the images appear together as shown below. Ideally review & profile datasets are put together so only one profilepicture appears for each review.
Is there a way to fix this issue?
models.py
class Product(models.Model):
name = models.CharField(max_length=100)
brand = models.CharField(max_length=100)
cost = models.DecimalField(max_digits=8, decimal_places=2, default=0.00)
category = models.CharField(max_length=100)
releasedate = models.DateField()
description = models.TextField()
productphoto = models.ImageField(default='products/default_product.jpg', upload_to='products')
class Review(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
profile = models.ForeignKey(Profile, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
rating = models.PositiveSmallIntegerField(default=1, validators = [MinValueValidator(1), MaxValueValidator(5)])
reviewtext = models.TextField()
class Profile(models.Model):
user = models.OneToOneField(User, on_delete = models.CASCADE)
profilephoto = models.ImageField(default='profiles/default_profile.jpg', upload_to='profiles')
views.py
class ProductDetailView(TemplateView):
# template_name = 'reviewApp/test.html'
template_name = 'reviewApp/product_detail.html'
def get_context_data(self, **kwargs):
prod = self.kwargs['pk']
context = super(ProductDetailView, self).get_context_data(**kwargs)
context['Products'] = Product.objects.filter(id=prod)
context['Reviews'] = Review.objects.filter(product=prod)
profile_ids = Review.objects.filter(product=prod).values_list('profile_id', flat=True)
context['Profiles'] = Profile.objects.filter(id__in=profile_ids)
return context
product.html
{% for prod in Products %}
<img src="{{prod.productphoto.url}}">
{{ prod.brand }} {{ prod.name }}
£{{ prod.cost }}
{{ prod.category }}
{{ prod.releasedate }}
{{ prod.description }}
{% endfor %}
{% for rev in Reviews %}
{% for prof in Profiles %}
<img src="{{prof.profilephoto.url }}">
{% endfor %}
{{ rev.rating }}
{{ rev.author }}
{{ rev.reviewtext }}
{% endfor %}
view
You can query the profile picture as rev.profile.profilephoto
So, in my models I have:
class Ingredient(models.Model):
name = models.CharField(max_length=200)
articleNumber = models.IntegerField(unique=True)
costPerUnity = models.DecimalField(max_digits=4, decimal_places=2)
class Recipe(models.Model):
name = models.CharField(max_length=200)
ingredients = models.ManyToManyField(Ingredient, through='Recipe_Ingredient', related_name='recipes')
class Recipe_Ingredient(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE)
quantity = models.FloatField()
GRAM = 'g'
KILOGRAM = 'kg'
LITER = 'l'
CENTILITER = 'cl'
UNITY_CHOICES = (
(GRAM, 'Gram(s)'),
(KILOGRAM, 'Kilogram(s)'),
(LITER, 'Liter(s)'),
(CENTILITER, 'Centiliter(s)'),
)
quantityUnit = models.CharField(
max_length=2,
choices=UNITY_CHOICES,
default=GRAM,
)
In my template:
{% for ingredient in recipe.ingredients.all %}
<li>{{ ingredient.name }} - # quantity goes here </li>
{% endfor %}
How can I show the quantity atribute of the Recipe_Ingredient associated with this recipe and ingredient?
In the shell I could do this query: Recipe_Ingredient.objects.get(ingredient=Ingredient.objects.get(name='Cenoura'), recipe=Recipe.objects.get(name='Teste')), but I'm not quite sure how to do it in the template and what's the correct way of doing it.
You can accomplish what you want by iterating over the Receipe_Ingredient relationship.
{% for recipe_ingredient in recipe.recipe_ingredient_set.all %}
<li>{{ recipe_ingredient.ingredient.name }} - {{ recipe_ingredient.quantity }} </li>
{% endfor %}
I can't seem to figure this out. My view takes an argument for Team.id, and I want to return a context object with each User object with a certain value in User.profile.team and the associated date from Reports. I feel like I started on the right track, but am missing something. The output of my template contains all the data I'm trying to get, but not in a way that can be displayed logically.
Basically, I'm using models similar to the following:
class Reports(models.Model):
user = models.ForeignKey(User, null=True, on_delete=models.PROTECT)
product = models.CharField(max_length=15)
apps_activated = models.IntegerField(blank=True, null=True)
prem_submitted = models.DecimalField(max_digits=30, decimal_places=2)
class Team(models.Model):
name = models.CharField(max_length=255)
leader = models.ForeignKey(User,on_delete=models.PROTECT)
Extended user profile:
class Profile(models.Model):
COORDINATOR = 1
LEADER = 2
ADMIN = 3
ROLE_CHOICES = (
(COORDINATOR, 'Coordinator'),
(LEADER, 'Leader'),
(ADMIN, 'Admin'),
)
user = models.OneToOneField(User, on_delete=models.CASCADE)
team = models.ForeignKey(Team, on_delete=models.PROTECT,null=True)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, null=True, blank=True)
The closest I got in my view to return the desired data is with the following:
team = 1
team_name = Team.objects.get(id=team)
team_users = User.objects.filter(profile__team=team).all()
team_stats = []
for user in team_users:
team_stats.append(Reports.objects.filter(user_id=user.id))
With a template that looks like:
{% block content %}
<h1>{{ team }}</h1>
<ul>
{% for user in team_users %}
<li>{{ user.first_name }} {{ user.last_name }}</li>
{% endfor %}
</ul>
<ul>
{% for stat in team_stats %}
<li>
{% for line in stat %}
{{ line.product }} {{ line.type }} #etc #etc
{% endfor %}
</li>
{% endfor %}
</ul>
{% endblock %}
I thought I was on to something with prefetch_related(), but couldn't figure it out. Ideally, I'd only have to return one context object to my template.
Edit:
If it makes it more clear, this query returns the results I'm attempting to pass to the template:
select auth_user.first_name, auth_user.last_name, r.product, r.apps_activated, r.prem_submitted, r.conversion_percentage, r.type
from auth_user
join home_profile
on auth_user.id = home_profile.user_id
join reports_reports as r
on auth_user.id = r.user_id
where home_profile.team_id = 1
which returns rows that look like:
first_name-last_name-product-apps_activated-prem_submitted-conversion_rate-type
user_1-user_1_last-product_1-693-139764.00-53.86-type1
user_1-user_1_last-product_2-74-27400.10-0.00-type1
user_1-user_1_last-product_3-102-19782.00-47.00-type2
user_2-user_2_last-product_1-7-2437.70-0.00-type2
user_2-user_2_last-product_2-52-10608.00-42.54-type3
user_2-user_2_last-product_3-260.40-0.00-type3
Potential Solution 1:
So, I would do the following. Change user in your Reports model to link explicitly to Profile (which, in turn, links to the user)
class Reports(models.Model):
profile = models.ForeignKey(Profile, null=True, on_delete=models.PROTECT)
product = models.CharField(max_length=15)
apps_activated = models.IntegerField(blank=True, null=True)
prem_submitted = models.DecimalField(max_digits=30, decimal_places=2)
It seems likely that you are looking for a are probably looking for something like a select_related() query, adding in an extra ForeignKey field for the Profile:
reports = Reports.objects.select_related('profile')
You can check the resulting SQL via str(reports.query), which should result in sth along the lines of what you outlined in your question.
The returned cursor values are then translated into the appropriate ORM model instances, so that when you loop over these reports, you access the related tables' values via their own objects. However, these accesses along the pre-selected forward relations will not cause extra db hits:
{% for report in reports %}
{{ report.profile.user.username }}
{{ report.product }}
# ...
{% endfor %}
Let me know how you get on, and if we can brainstorm a solution more suitable, if this one isn't.
Potential Solution 2:
Perhaps another solution, and maybe the simplest of the two, would be to have a ManyToMany relationship in your Profile model to Reports:
class Profile(models.Model):
COORDINATOR = 1
LEADER = 2
ADMIN = 3
ROLE_CHOICES = (
(COORDINATOR, 'Coordinator'),
(LEADER, 'Leader'),
(ADMIN, 'Admin'),
)
user = models.OneToOneField(User, on_delete=models.CASCADE)
team = models.ForeignKey(Team, on_delete=models.PROTECT,null=True)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, null=True, blank=True)
reports = models.ManyToManyField(Reports, ...)
Then you should be able to loop over users in the template:
{% for user in users %}
{% for report in user.reports.all %}
{{ report.product }}
{% endfor %}
{% endfor %}
In my view.py, my context look like this:
context = {'books': books, 'asked_author': asked_author, 'role': role_field_list}
books is a list of dictionary (a list of book) and for each book, it exists one or more keys for which the key is named by a name in role_field_list:
I try to execute this template:
{% for book in books %}
{% for name in role %}
<h1>{{name}}</h1>
{% for authors in book.name %}
{{ authors.lastname }} {{authors.firstname}}
{% endfor %}
{% endfor %}
{% endfor %}
But book.name doesn't worked.
It takes name as a litteral 'name' not as a variable...
Thank you for any help.
To have more details about "field" and "type" of my context dictionnary, this my view.py:
def booksAuthor(request, author):
books_role = AuthorRoleBook.objects.filter(author_book=author).values() # get all the books written by one author
asked_author = AuthorBook.objects.get(pk=author) # get lastname and firstname for the author selected
books=[]
for book_role in books_role:
book = Book.objects.get(pk=book_role['book_id'])
book_dict = model_to_dict(book)
authors_role = AuthorRoleBook.objects.filter(book_id=book).values() # Get id of the different contributor for each book
role_field_list = ['auteur', 'traducteur', 'editeur_scientifique', 'directeur_publication']
for name in role_field_list:
list_author=[]
for author_role in authors_role:
if author_role['role_id']==name:
author=AuthorBook.objects.get(pk=author_role['author_book_id'])
author_dict = model_to_dict(author)
list_author.append(author_dict)
else:
pass
book_dict[name]=list_author
books.append(book_dict)
print(book_dict)
context = {'books': books, 'asked_author': asked_author, 'role': role_field_list}
return render(request, 'books_author.html', context)
And for my models, maybe it is not the easier way but this is:
class AuthorRoleBook(models.Model):
author_book = models.ForeignKey(AuthorBook)
role = models.ForeignKey('Role')
book = models.ForeignKey('Book')
class AuthorBook(models.Model):
lastname = models.CharField(max_length=100, blank=True, null=True)
firstname = models.CharField(max_length=100, blank=True, null=True)
.... ....
unique_together = (('lastname', 'firstname'),)
class Role(models.Model):
id = models.CharField(primary_key=True, max_length=100)
class Book(models.Model):
titre = models.TextField(blank=True, null=True)
....... .... ...
isbn_electronique = models.CharField(max_length=250, blank=True, null=True)
Thank you in advance.
For me it is working now. I keep the same views.py but in my template I do this:
{% for book in books %}
<h4>{{ book.titre }}</h4>
{% if book.directeur_publication %}
Directeur Publication:
{% for name in book.directeur_publication %}
{{ name.lastname }} {{name.firstname}}
{% endfor %}
{% endif %}
{% if book.editeur_scientifique %} ....
...
.....Etc
And for each value in role_field_list (['auteur', 'traducteur', 'editeur_scientifique', 'directeur_publication']) I put manually the value n my template in a "if condition".
It is not really a beautiful way... but it is working. For sure to use model object and not dictionary will be better.
Thanks for your answer, nevertheless if you have a smart code or idea to do that...