Reference multiple foreign keys in Django Model - python

I'm making a program that helps log missions in a game. In each of these missions I would like to be able to select a number of astronauts that will go along with it out of the astronauts table. This is fine when I only need one, but how could I approach multiple foreign keys in a field?
I currently use a 'binary' string that specifies which astronauts are to be associated with the mission (1 refers to Jeb, but not Bill, Bob, or Val and 0001 means only Val), with the first digit specifying the astronaut with id 1 and so forth. This works, but it feels quite clunky.
Here's the model.py for the two tables in question.
class astronauts(models.Model):
name = models.CharField(max_length=200)
adddate = models.IntegerField(default=0)
experience = models.IntegerField(default=0)
career = models.CharField(max_length=9, blank=True, null=True)
alive = models.BooleanField(default=True)
def __str__(self):
return self.name
class Meta:
verbose_name_plural = "Kerbals"
class missions(models.Model):
# mission details
programid = models.ForeignKey(programs, on_delete=models.SET("Unknown"))
missionid = models.IntegerField(default=0)
status = models.ForeignKey(
missionstatuses, on_delete=models.SET("Unknown"))
plan = models.CharField(max_length=1000)
# launch
launchdate = models.IntegerField(default=0)
crewmembers = models.IntegerField(default=0)
# recovery
summary = models.CharField(max_length=1000, blank=True)
recdate = models.IntegerField(default=0)
def __str__(self):
return str(self.programid) + '-' + str(self.missionid)
class Meta:
verbose_name_plural = "Missions"
I saw a post about an 'intermediate linking table' to store the crew list but that also isn't ideal.
Thanks!

This is the use case for Django's ManyToManyField. Change the appropriate field on the missions:
class missions(models.Model):
crewmembers = models.ManyToManyField('astronauts')
You can access this from the Astronaut model side like so:
jeb = astronaut.objects.get(name='Jebediah Kerman')
crewed_missions = jeb.missions_set.all()
Or from the mission side like so:
mission = missions.objects.order_by('?')[0]
crew = mission.crewmembers.all()
This creates another table in the database, in case that is somehow a problem for you.

Related

Django form: Update model values based on user input in one field

I am writing a form to let a user enter a purchase from the template. A couple things need to happen:
the purchase goes to populate a row in the replenishment table
some fields of the replenishment table get updated based on what the user has input
here is what my model look like:
class replenishment(models.Model):
Id = models.CharField(max_length=100, primary_key=True, verbose_name= 'references')
Name = models.CharField(max_length=200)
Quantity = models.FloatField(default=0)
NetAmount = models.FloatField(default=0)
SupplierID = models.CharField(max_length=200)
Supplier = models.CharField(max_length=200)
SellPrice = models.FloatField(default=0)
StockOnOrder = models.FloatField(default=0)
StockOnHand = models.FloatField(default=0)
def __str__(self):
return self.reference
and the form:
class ProcurementOperationRecord(forms.Form)
Id = forms.CharField(required=True)
Quantity = forms.FloatField(required=True)
NetAmount = forms.FloatField(required=True)
Supplier = forms.CharField(required=True)
SellPrice = forms.FloatField(required=True)
I have no clue how to let the user input the values in form and automatically add Quantity to StockOnOrder as well as automatically recognize the SupplierID based on Supplier. At this point I don't know where to start really. At least, is it possible to achieve what I try to do?
First, I've changed some things around and added some comments to what and why I did them.
# models/classes in python are singular AND camel cased (99.9%)
class Supplier(models.Model):
...
# models/classes in python are singular AND camel cased (99.9%)
class Replenishment(models.Model):
# attributes are normally lower case and snake cased (99.9%)
# try not to do this, a CharField??, unless you're using a guid? if so use UUIDField()
# https://docs.djangoproject.com/en/3.1/ref/models/fields/#uuidfield
id = models.CharField(db_column='Id', max_length=100, primary_key=True, verbose_name='references')
name = models.CharField(db_column='Name', max_length=200)
quantity = models.FloatField(db_column='Quantity', default=0)
net_amount = models.FloatField(db_column='NetAmount', default=0)
# deleted your field "Supplier" -- with this change you can join to the other table and get what you need without having to duplicate anything
supplier = models.ForeignKey(Supplier, db_column='SupplierID')
sell_price = models.DecimalField(db_column='SellPrice', default=0, max_digits=6, decimal_places=2) # You're asking for trouble if you keep this as FloatField
stock_on_order = models.IntegerField(db_column='StockOnOrder', default=0) # how can you have ordered a .5 for your stock? changed to IntegerField
stock_on_hand = models.IntegerField(db_column='StockOnHand', default=0) # how can you have a .5 of your stock? changed to IntegerField
class Meta:
db_table = 'replenishment' # try not to do this either.. let django come up with the name.. unless you're using an existing database/table?
...
# models/classes in python are singular AND camel cased (99.9%)
# django has a standard that they normally postfix forms with "Form" at the end of the class (no matter if it's a ModelForm or regular Form)
class ProcurementOperationRecordForm(forms.ModelForm)
class Meta:
model = Replenishment
fields = ('id', 'quantity', 'net_amount', 'supplier', 'sell_price')
# I would remove the "id", the client shouldn't care or know about it..
Now to create and update. (This would live inside a view)
# creating?
form = ProcurementOperationRecordForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(..) or render(..)
# updating?
replenishment = Replenishment.objects.get(id='...something')
form = ProcurementOperationRecordForm(data=request.POST, instance=replenishment)
if form.is_valid():
form.save()
return redirect(..) or render(..)
This is just a general idea. You can try something like this.
First get the user input values of quantity and supplier like this from the valid form.
quantity = form.cleaned_data.get('quantity')
supplier = form.cleaned_data.get('supplier')
Then you can update your replenishment model
replenishment.objects.filter(Supplier=supplier).update(StockOnOrder=quantity)

Django - ForeignKey "Reverse" ("_set")

I need to cruise back in reverse through my Foreignkey. For some reason it doesn't work...
models.by :
class Investment(models.Model):
feeder = models.ForeignKey(Fund, on_delete=models.CASCADE)
investor = models.ForeignKey(Investor, on_delete=models.CASCADE)
(...)
class Fund(models.Model):
slug = models.CharField(max_length=100, default = 'default')
(...)
class Investor(models.Model):
account = models.CharField(max_length = 10, unique=True)
first_name = models.CharField(max_length = 100)
(...)
views.py :
Investment.objects.filter(investor__pk=pk)
--> I need to get to the slug attribute of Fiund of each Investment of an investor.. I have already filtered on investor (that's ok). Next is cruising to the slug. I assume I need to work with fund.set_all() but it doesn't work.. Anyone to the rescue?
Thanks !!!
I think you are a bit confused by how this works. To work your way from Investor -> Funds, you would have to go through Investor -> Investment -> Funds. You can do this like so:
First, use related_name:
class Investment(models.Model):
feeder = models.ForeignKey(Fund, on_delete=models.CASCADE, related_name="investmentAsFeeder")
investor = models.ForeignKey(Investor, on_delete=models.CASCADE, related_name="investmentAsInvestor")
Then, you can use it like so:
investor = Investor.objects.get(id=1)
investors_investments = investor.investmentAsInvestor.all()
investor_investment_funds = Fund.objects.filter(investmentAsFeeder__in=investor_investments)
If you need values only you can get it with one query:
my_investor = Investor.objects.get(pk=pk)
my_investor.invesment_set.values('id', 'feeder__slug`)
Or you can have another query if you don't need investor instance itself:
Investment.objects.filter(investor__pk=pk).values('id', 'feeder__slug')

Django ForeignKey accept two models

I'm working on this big project with Django and I have to update the database. I have to add another table which will replace another later.
So I want to add in a model the possibility to have a field where I can have either the old model OR the new one.
Here is the code of the old model:
class Harvests(models.Model):
ident_culture = models.IntegerField(primary_key=True)
intitule_culture = models.CharField(max_length=50, blank=True)
nom_fertiweb = models.CharField(max_length=50, blank=True, null = True)
affichage_quintaux_tonne = models.CharField(max_length=1,
choices=RENDEMENT_CHOICES, default = 'T')
type_culture = models.ForeignKey("TypeCulture", null=True)
slug = models.SlugField(null=True, blank=True)
image = models.ImageField(upload_to = 'images_doc_culture/',
null=True, blank = True)
affichage = models.BooleanField(default = True)
class Meta:
verbose_name = "Liste - Culture"
verbose_name_plural = "Liste - Cultures"
ordering = ['intitule_culture']
def __str__(self):
return self.intitule_culture
def label(self):
return self.intitule_culture or ''
#classmethod
def get_choices(cls):
choices = [('', corp.EMPTY_CHOICE_LBL)]
c_category_lbl, c_category = '', []
for item in cls.objects.all():
choices.append((item.pk, item.intitule_culture))
return choices
And there is the code od the new one I created:
class Crops(models.Model):
intitule_culture = models.CharField(max_length=75, blank=True)
affichage_quintaux_tonne = models.CharField(max_length=2,
choices=RENDEMENT_CHOICES, default = 'T')
type_culture = models.ForeignKey("TypeCulture", null=True)
ident_culture = models.IntegerField(primary_key=True)
affichage = models.BooleanField(default = True)
id_marle = models.IntegerField(null=True)
class Meta:
verbose_name = "Liste - Culture 2019"
verbose_name_plural = "Liste - Cultures 2019"
ordering = ['intitule_culture']
def __str__(self):
return self.intitule_culture
def label(self):
return self.intitule_culture or ''
#classmethod
def get_choices(cls):
choices = [('', corp.EMPTY_CHOICE_LBL)]
c_category_lbl, c_category = '', []
for item in cls.objects.all():
choices.append((item.pk, item.intitule_culture))
return choices
I want to accept both models in the field culture in this model:
class CompanyHarvest(models.Model):
company = models.ForeignKey('corp.Company', verbose_name='Exploitation',
related_name ='cultures')
culture = models.ForeignKey(Harvests, verbose_name ='Culture')
precision = models.CharField(max_length=255, blank=True)
saison_culture = models.CharField(max_length=1, choices=SAISON_CHOICES,
default = 'P')
class Meta:
verbose_name = "Expl. - Culture"
verbose_name_plural = "Expl. - Cultures"
unique_together = ('company', 'culture', 'precision', 'saison_culture')
def __str__(self):
return str(self.culture) + ' ' + self.precision + \
' ' + str(self.get_saison_culture_display() )
#property
def slug(self):
return "_".join([slugify(str(self.culture or '')),
slugify(str(self.precision or ''))]
)
I'm new to Django, can anyone help me with this please ? (^-^)
This is not possible - at least not this way. And this is not a Django limitation but a SQL one, a foreign key cannot reference either one table or another.
A possible and simple obvious solution here would be to have two foreign keys in CompanyHarvest - one for each of the old and new model -, each with blank=True et default=None, but it can quickly make a mess of all the client code (all code using CompanyHarvest).
Much better solutions would be to either only keep the existing model (adding any new field/feature to it and eventually hiding obsolete ones) or migrate all old model records to the new model (this can be combined with the naive "two foreign keys" solution so you can keep the old table and records as archives if necessary).
Also - totally unrelated but -, this:
#classmethod
def get_choices(cls):
choices = [('', corp.EMPTY_CHOICE_LBL)]
c_category_lbl, c_category = '', []
for item in cls.objects.all():
choices.append((item.pk, item.intitule_culture))
return choices
1/ should be defined on the manager (cf https://docs.djangoproject.com/en/2.1/topics/db/managers/#adding-extra-manager-methods)
2/ should be written using .values() queryset (which will save on both the db query and building full-blown instances for no good reason):
for item in cls.objects.values("pk", "intitule_culture"):
choices.append(item)
3/ and could very possibly (i'd have to see how it's used) replaced by a ModelChoiceField in the calling code.
Oh and yes: if you allow blanks for text fields, you very probably want to force the empty string as default so you don't two possible (and incompatible) cases (sql NULL and the empty string) when no value is given.

django querying from 3 models

My models are :
model 1:
class source_of_enquiry(models.Model):
source_of_enquiry = models.CharField(max_length=200, null=True, blank=True)
def __unicode__(self):
return '%s' % self.source_of_enquiry
model 2:
class customers(models.Model):
cutomer_name = models.CharField(max_lentgth=200)
customer_src_n_type = models.Foreign_key(source_of_enquiry)
customer_contact = models.CharField(max_lentgth=200)
def __unicode__(self):
return '%s' % self.customer_name
model 3:
class sales_cycle(models.Model):
item_name = models.CharField(max_length=200)
customer_name = models.Foreignkey(customers)
def __unicode__(self):
return '%s' % self.item_name
how should i know how many sales had peen completed based on source of enquiry??
tried many from `select_related' and 'prefetch_selected' , but all were kaput.
First of all - python naming convention state that classes should not have underscores and prefer upper-case letters instead. So your models should be SourceEnquiry, Customer (not plural) and SaleCycle.
That being said, let's say I have a SourceEnquiry item (I'm going to pick one arbitrarily), and you want all related SaleCycle items, you do it like so:
>>> sinq = SourceEnquiry.objects.get(pk=1)
>>> SaleCycle.objects.all().filter(customer_name__customer_src_n_type=sinq)
p.s.
also, going back to the naming convention thing, it's redundant to use customer as part of a field name inside the class Customer. You alread know it's a customer object, so it's better to name it like so:
class Customer(models.Model):
name = models.CharField(max_lentgth=200)
src_n_type = models.Foreign_key(source_of_enquiry)
contact = models.CharField(max_lentgth=200)
You other fields can also be cleaner:
class SourceEnquiry(models.Model):
value = models.CharField(max_length=200, null=True, blank=True)
class SaleCycle(models.Model):
item = models.CharField(max_length=200)
customer = models.Foreignkey(Customer)

django queryset include more columns in select statement

I been trying to create a backward relation using queryset and the joining is working fine, accept that its not including the other joined table in the selected columns. Below is my models, queryset and query.str() print
class Main(models.Model):
slug = models.SlugField()
is_active = models.BooleanField(default=True)
site = models.ForeignKey(Site)
parent = models.ForeignKey('self', blank=True, null=True, limit_choices_to={'parent' : None})
class Meta:
unique_together = (("slug", "parent"))
def __unicode__(self):
return self.slug
class MainI18n(models.Model):
main = models.ForeignKey(Main)
language = models.CharField(max_length=2, choices=settings.LANGUAGES)
title = models.CharField(max_length=100)
label = models.CharField(max_length=200, blank=True, null=True)
description = models.TextField(blank=True, null=True)
disclaimer = models.TextField(blank=True, null=True)
class Meta:
unique_together = (("language", "main"))
def __unicode__(self):
return self.title
class List(models.Model):
main = models.ForeignKey(Main)
slug = models.SlugField(unique=True)
is_active = models.BooleanField(default=True)
parent = models.ForeignKey('self', blank=True, null=True)
def __unicode__(self):
return self.slug
class ListI18n(models.Model):
list = models.ForeignKey(List)
language = models.CharField(max_length=2, choices=settings.LANGUAGES)
title = models.CharField(max_length=50)
description = models.TextField()
class Meta:
unique_together = (("language", "list"))
def __unicode__(self):
return self.title
and my queryset is
Main.objects.select_related('main', 'parent').filter(list__is_active=True, maini18n__language='en', list__listi18n__language='en')
and this is what my query is printing
'SELECT `category_main`.`id`, `category_main`.`slug`, `category_main`.`is_active`, `category_main`.`site_id`, `category_main`.`parent_id`, T5.`id`, T5.`slug`, T5.`is_active`, T5.`site_id`, T5.`parent_id` FROM `category_main` INNER JOIN `category_maini18n` ON (`category_main`.`id` = `category_maini18n`.`main_id`) INNER JOIN `category_list` ON (`category_main`.`id` = `category_list`.`main_id`) INNER JOIN `category_listi18n` ON (`category_list`.`id` = `category_listi18n`.`list_id`) LEFT OUTER JOIN `category_main` T5 ON (`category_main`.`parent_id` = T5.`id`) WHERE (`category_maini18n`.`language` = en AND `category_list`.`is_active` = True AND `category_listi18n`.`language` = en )'
anyone can help show columns from list and listi18n? I tried extra but It doesn't allow me to pass things like category_list.*
thanks
UPDATE
Thanks for Daniel approach, I managed to get it to work but instead I had to start from ListI18n
ListI18n.objects.select_related('list', 'list__main', 'list__main__parent', 'list__main__i18nmain').filter(list__is_active=True, list__main__maini18n__language='en', language='en').query.__str__()
Its working perfectly now, but I couldn't include list_main_maini18n, below is the output query
'SELECT `category_listi18n`.`id`, `category_listi18n`.`list_id`, `category_listi18n`.`language`, `category_listi18n`.`title`, `category_listi18n`.`description`, `category_list`.`id`, `category_list`.`main_id`, `category_list`.`slug`, `category_list`.`is_active`, `category_list`.`parent_id`, `category_main`.`id`, `category_main`.`slug`, `category_main`.`is_active`, `category_main`.`site_id`, `category_main`.`parent_id`, T5.`id`, T5.`slug`, T5.`is_active`, T5.`site_id`, T5.`parent_id` FROM `category_listi18n` INNER JOIN `category_list` ON (`category_listi18n`.`list_id` = `category_list`.`id`) INNER JOIN `category_main` ON (`category_list`.`main_id` = `category_main`.`id`) INNER JOIN `category_maini18n` ON (`category_main`.`id` = `category_maini18n`.`main_id`) LEFT OUTER JOIN `category_main` T5 ON (`category_main`.`parent_id` = T5.`id`) WHERE (`category_list`.`is_active` = True AND `category_listi18n`.`language` = en AND `category_maini18n`.`language` = en )'
Any idea how can I include MainI18n in the query result? should I use extra and include the tables and do the relation in the where clause? or is there a better approach?
The relationship from Main to List is a backwards ForeignKey (ie the FK is on List pointing at Main), and select_related doesn't work that way. When you think about it, this is correct: there are many Lists for each Main, so it doesn't make sense to say "give me the one List for this Main", which is what select_related is all about.
If you started from List, it would work:
List.objects.select_related('main__parent').filter(is_active=True, main__maini18n__language='en', listi18n__language='en')
because that way you're only following forwards relationships. You may find you're able to reorder your views/templates to use the query this way round.
Looks to me it is actually working (T5 in your select statement). You can always access fields from related instances in django via something like my_obj.parent.is_active. If you used select_related before they are included in the first query. If you didn't specify it in select_related a call to my_obj.parent.is_active for example would perform an extra db query.

Categories