Django ManyToMany with through model implementation - python

So let's say I have these models in my Django app:
class Ingredient(models.Model):
name = models.CharField(max_length=100)
def __unicode__(self):
return self.name
class Recipe(models.Model):
name = models.CharField(max_length=100)
ingredients = models.ManyToManyField(Ingredient,
through='RecipeIngredient')
def __unicode__(self):
return self.name
class RecipeIngredient(models.Model):
recipe = models.ForeignKey(Recipe)
ingredient = models.ForeignKey(Ingredient)
quantity = models.DecimalField(max_digits=4, decimal_places=2)
unit = models.CharField(max_length=25, null=True, blank=True)
def __unicode__(self):
return self.ingredient.name
Now I want to access a recipe's ingredients (really RecipeIngredients). Via the Django shell:
>>> r = Recipe.objects.get(id=1)
>>> ingredients = RecipeIngredients.objects.filter(recipe=r)
This seems counter-intuitive and clunky to me. Ideally I'd like to be able to have a Recipe object and get RecipeIngredients from it directly.
Is there a more elegant implementation of my models? Is there any way to improve my existing model implementation?

Use related_name, just as you would with a normal ForeignKey or ManyToManyField:
class RecipeIngredients(models.Model):
recipe = models.ForeignKey(Recipe, related_name='ingredient_quantities')
And then:
>>> r = Recipe.objects.get(id=1)
>>> ingredients = r.ingredient_quantities.all()

Related

How can I implement the same widget that Django uses to ManyToMany fields in the admin page?

My models:
class Ingredient(models.Model):
BASE_UNIT_CHOICES = [("g", "Grams"), ("ml", "Mililiters")]
CURRENCY_CHOICES = [("USD", "US Dollars"), ("EUR", "Euro")]
ingredient_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=200)
base_unit = models.CharField(max_length=2, choices=BASE_UNIT_CHOICES)
cost_per_base_unit = models.FloatField()
currency = models.CharField(
max_length=3, choices=CURRENCY_CHOICES, default="EUR")
def __str__(self):
return self.name
class RecipeIngredient(models.Model):
quantity = models.FloatField()
ingredient_id = models.ForeignKey(Ingredient, on_delete=models.CASCADE)
def __str__(self):
return f"{self.quantity} / {self.ingredient_id}"
class Recipe(models.Model):
recipe_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=200)
ingredients = models.ManyToManyField(RecipeIngredient)
date_created = models.DateTimeField('Date Created')
def __str__(self):
return f"{self.name}, {self.ingredients}"
When I use the admin page, it has this + button that allows me to create new ingredient/quantity combinations
like this
But when I try to use it from a form in my code it looks like
this
Here is my form code:
class AddRecipeForm(forms.ModelForm):
class Meta:
model = Recipe
fields = ['name', 'ingredients', 'date_created']
You should write the 'widgets' for each field in you Form that need configuration.
Check the documentation 'Widgets in forms', or even, you can define your own Widgets.

Can't I add a list to my models.py in django?

I'm quite new in Django and in this project I need to manage information about a restaurant. The problem is that I can't add more than one ingredient required to a recipe
My models.py is this:
from django.db import models
# Create your models here.
class Ingredient(models.Model):
name = models.CharField(max_length=35)
quantity = models.IntegerField()
unit = models.CharField(max_length=5)
unit_price = models.FloatField()
def __str__(self):
return self.name
class MenuItem(models.Model):
title = models.CharField(max_length=35)
price = models.FloatField()
example = models.ImageField()
def __str__(self):
return self.title
class RecipeRequirement(models.Model):
menu_items = models.ForeignKey(MenuItem, default = 1, on_delete=models.SET_DEFAULT)
ingredient = models.CharField(max_length=35)
quantity = models.FloatField()
def __str__(self):
return str(self.menu_items)
class Purchase(models.Model):
menu_item = models.ForeignKey(MenuItem, default=1, on_delete=models.SET_DEFAULT)
timestamp = models.DateTimeField()
def __str__(self):
return 'Purchase ' + str(self.menu_item) + str(self.timestamp)
After replacing ingredients = models.CharField() for ingredients = models.ManyToManyField() a list of all ingredients appear:
But when I add a new ingredient to my Ingredient model, it automatically appears in every single menu_item:
You need to have a ManyToMany relationship in your RecipeRequirement model. This will allow you to select multiple ingredients for any one RecipeRequirement object. Read more here: Django ManyToMany Fields
class RecipeRequirement(models.Model):
menu_items = models.ForeignKey(MenuItem, default = 1, on_delete=models.SET_DEFAULT)
ingredients = models.ManyToManyField(Ingredient)
quantity = models.FloatField()
def __str__(self):
return str(self.menu_items)
Set ingredients in the RecipeRequirement class as a ManyToMany field on the Ingredient class:
class RecipeRequirement(models.Model):
menu_items = models.ForeignKey(MenuItem, default = 1, on_delete=models.SET_DEFAULT)
ingredients = models.ManyToManyField(Ingredient)
quantity = models.FloatField()
def __str__(self):
return str(self.menu_items)
I believe that your Ingredientclass must have a ForeignKey associated with the RecipeRequirement in order to add more than one ingredient related to the recipe.
I suggest using a website like https://drawsql.app/ in order to obtain a better visualization of your database relationships.

relationships in django 2 models.py

I want to define a relationship between Book and Member through Borrow in models.py
ER
But I don't know how to define the Borrow relationship.
In the Borrow table it must be determined which books have been borrowed by who and which books have been returned on which date. Should I use another table for this date field?
models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext as _
class CategoryType(models.Model):
category_name = models.CharField(max_length=200)
def __str__(self):
return self.category_name
class Book(models.Model):
name = models.CharField(verbose_name="عنوان", max_length=128)
number_of_copy = models.IntegerField(default=0)
writer = models.CharField(max_length=64)
B_category = models.ForeignKey(CategoryType, on_delete=models.CASCADE)
class Meta:
ordering = ["B_category"]
def __str__(self):
return self.name
class Borrow(models.Model):
borrowed_from_date = models.DateField(_("borrow Date"), default=0)
borrowed_to_date = models.DateField(_("return Date"), default=3)
actual_return_date = models.DateField()
borrowed_by = models.ForeignKey(member, on_delete=models.CASCADE)
books = models.ManyToManyField(Book)
def __str__(self):
return self.id
class Member(AbstractUser):
pass
I think in the Member class I should have a field containing borrow_id, but how?
It seems to me that you need to use a ManyToMany relationship with a through model (this way you can store extra information for every row of the Borrow model)
...
class Borrow(models.Model):
borrowed_from_date = models.DateField(_("borrow Date"), default=0)
borrowed_to_date = models.DateField(_("return Date"), default=3)
actual_return_date = models.DateField()
borrowed_by = models.ForeignKey(Member, on_delete=models.CASCADE)
book = models.ForeignKey(Book)
def __str__(self):
return self.id
...
class Member(AbstractUser):
borrowed_books = models.ManyToManyField(Book, through='Borrow')
Maybe this link (https://docs.djangoproject.com/es/2.1/ref/models/fields/#django.db.models.ManyToManyField.through) could clarify it more.

Django- limit_choices_to using 2 different tables

I fear that what I am trying to do might be impossible but here we go:
Among my models, I have the following
Class ParentCategory(models.Model):
name = models.CharField(max_length=128)
def __unicode__(self):
return self.name
Class Category(models.Model):
parentCategory = models.ForeignKey(ParentCategory, on_delete=models.CASCADE, )
name = models.CharField(max_length=128)
def __unicode__(self):
return self.name
Class Achievement(models.Model):
milestone = models.ForeignKey(Milestone, on_delete=models.CASCADE)
description = models.TextField( )
level_number = models.IntegerField()
completeion_method = models.ForeignKey(Category, on_delete = models.CASCADE, limit_choices_to={'parentCategory.name':'comp method'})
def __unicode__(self): # TODO:
return description[0,75] + '...'
I know the completion method field throws an error because it is not correct syntax. But is there a way to achieve the wanted result using a similar method?
Maybe this will work:
limit_choices_to={'parentCategory__name': 'comp method'}

Django sharing one model between two others

I built a system to review wines and foods. I quickly found myself repeating models and templates with tiny differences.
Fundamentally it seems I want a review to relate to either a food or a wine. And each food or wine can have many reviews.
I had an FK (current way) to both and just left one blank but given they're so similar I decided that wasn't wise.
I then went to abstract models to at least generify the fields (new way) but as I couldn't link to the generic item model I had a slightly more elegant code base for the same problem.
Researching into this I'm wondering if a generic relation from the food and wine to the review is the way to go or maybe content types but I don't get quite how they work or if they are what I'm looking for.
Current way - Wines have brands, Foods have stores and Reviews have Foods and wines
class Brand(models.Model):
brand_name = models.CharField(max_length=30)
location = models.ForeignKey(Location, null=True,blank=True)
import datetime
YEAR_CHOICES = []
for r in range(2005, (datetime.datetime.now().year+1)):
YEAR_CHOICES.append((r,r))
YEAR_CHOICES = list(reversed(YEAR_CHOICES))
class Wine(models.Model):
wine_name = models.CharField(max_length=30)
wine_type = models.ForeignKey(WineType)
wine_year = models.IntegerField( choices=YEAR_CHOICES, default=datetime.datetime.now().year)
brand = models.ForeignKey(Brand)
class Store(models.Model):
store_name = models.CharField(max_length=30)
def __str__(self):
return self.store_name
class Food(models.Model):
food_name = models.CharField(max_length=30)
food_desc = models.CharField(blank=True,max_length=100)
store = models.ForeignKey(Store)
def __str__(self):
return self.store.store_name +' - '+self.food_name
class Review(models.Model):
rating = models.CharField(max_length=30)
value = models.CharField(max_length=30)
date = models.DateField(auto_now_add=True)
person = models.ForeignKey(Person)
comment = models.CharField(blank=True,max_length=100)
food = models.ForeignKey(Food, blank=True,default=None,null=True)
wine = models.ForeignKey(Wine, blank=True,default=None,null=True)
class Meta():
ordering = ['-date']
New Way - Wines and Foods are Items, Stores and Brands are Sources, but reviews still need both Wines and foods
class Source(models.Model):
name = models.CharField(max_length=30)
desc = models.CharField(blank=True,max_length=100)
class Meta:
abstract = True
class Item(models.Model):
name = models.CharField(max_length=30)
desc = models.CharField(blank=True,max_length=100)
class Meta:
abstract = True
class WineSource(Source):
location = models.ForeignKey(Location, null=True,blank=True)
class Meta():
ordering = ['location', 'name']
class FoodSource(Source):
def __str__(self):
return self.name
import datetime
YEAR_CHOICES = []
for r in range(2005, (datetime.datetime.now().year+1)):
YEAR_CHOICES.append((r,r))
YEAR_CHOICES = list(reversed(YEAR_CHOICES))
class Wine(Item):
wine_type = models.ForeignKey(WineType)
wine_year = models.IntegerField( choices=YEAR_CHOICES, default=datetime.datetime.now().year)
source = models.ForeignKey(WineSource)
def __str__(self):
return self.source.name +' '+self.name+ ' ' + str(self.wine_type)+ ' '+ str(self.wine_year)
class Food(Item):
source = models.ForeignKey(FoodSource)
def __str__(self):
return self.source.name +' - '+self.name
class Review(models.Model):
rating = models.CharField(max_length=30)
value = models.CharField(max_length=30)
date = models.DateField(auto_now_add=True)
person = models.ForeignKey(Person)
food = models.ForeignKey(Food, blank=True,default=None,null=True)
wine = models.ForeignKey(Wine, blank=True,default=None,null=True)
#Doesn't work as it's abstract- item = models.ForeignKey(Item,null=True)
class Meta():
ordering = ['-date']
I think Generic Foreign Key is the answer. Something like:
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
class Product(models.Model):
...
class Food(Product):
...
class Wine(Product):
...
class Review(models.Model):
...
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
This allows us to relate a review to any single record of any model in our project. The content_type field tracks which model you are trying to relate to (Food or Wine in this case). The object_id field track which record in the Wine or Food table we are trying to track. content_object is a convenience attribute that allows us direct access to the object (once the review has been saved).
When creating a new review you just assign the Wine or Food to the content_object field:
wine = Wine.objects.get(...)
review = Review(..., content_object=wine)
review.save()
You could also use Multi-table inheritance instead of an AbstractClass for Item. Then you can set a direct ForeignKey to Item in Review.
You can also combine this with InheritanceManager:
from model_utils.managers import InheritanceManager
class Item(models.Model):
name = models.CharField(max_length=30)
desc = models.CharField(blank=True,max_length=100)
objects = InheritanceManager()
class Wine(Item):
wine_type = models.ForeignKey(WineType)
wine_year = models.IntegerField( choices=YEAR_CHOICES, default=datetime.datetime.now().year)
source = models.ForeignKey(WineSource)
def __str__(self):
return self.source.name +' '+self.name+ ' ' + str(self.wine_type)+ ' '+ str(self.wine_year)
class Food(Item):
source = models.ForeignKey(FoodSource)
def __str__(self):
return self.source.name +' - '+self.name
class Review(models.Model):
rating = models.CharField(max_length=30)
value = models.CharField(max_length=30)
date = models.DateField(auto_now_add=True)
person = models.ForeignKey(Person)
item = models.ForeignKey(Item,null=True)
class Meta():
ordering = ['-date']
Then you can filter like this:
wine_reviews = Review.objects.exclude(item__wine__isnull=True)
food_reviews = Review.objects.exclude(item__food__isnull=True)
# and all item (directly sub-classed as wine or food:
items = Item.objects.select_subclasses().all()

Categories