ManytoManyField save and calculate - python

I just started self learning Python and Django as a hobby recently, and have been trying to self develop a project which helps me in my construction business. I have developed certain functions in my project which give me the result I want, but is not the most ideal in terms of coding practises. However, I am just learning from scratch and modifying my code as I learn.
However, now I am stuck (maybe because my basic concept is only not correct?). Require help on how to proceed.
So here is my models.py
class FootingQuantity(models.Model):
member_name = models.CharField(max_length=8, unique=True)
--more fields hidden--
x_axis = models.FloatField()
y_axis = models.FloatField()
class BeamQuantity(models.Model):
work = models.ForeignKey(Work, default=1, on_delete=models.CASCADE)
member_name = models.CharField(max_length=8, unique=True)
location = models.ManyToManyField(FootingQuantity)
length = models.FloatField(blank=True)
breadth = models.FloatField()
height = models.FloatField()
-- more fields --
#property
def length_of_beam(self):
yy = self.location.all().values_list('y_axis', flat=True)
xx = self.location.all().values_list('x_axis', flat=True)
ylist = list(yy)
xlist = list(xx)
return abs(ylist[1] - ylist[0] + xlist[1] - xlist[0])
#property
def total_concrete_quantity(self):
return float(self.length) * float(self.breadth) * float(self.width)
def save(self, *args, **kwargs):
self.length = self.length_of_beam
self.total_quantity = self.total_concrete_quantity
super(BeamQuantity, self).save(*args, **kwargs)
def __float__(self):
return self.length, self.total_quantity
I want my model to accept ManytoManyRelation of 2 Footings selected and then calculate the length. The length will then multiply with height and width to give me the total quantity (There are other calculations as well but I am guessing they are all just crashing due to not getting length).
Currently, when I fill the Form or try to fill in details through the admin page and click on Save, I get a ValueError
ValueError - "<BeamQuantity: B5>" needs to have a value for field "id"
before this many-to-many relationship can be used.
I think the ManytoManyField needs to save and then the calculation needs to run. Currently both are trying to occur simultaneously and hence the error. Please help.

Exactly, you need to create your BeamQuantity instance first, before calculating the length of beans. Leave those fields empty for a second.
To do that, I recommend you to let all the fields that require the many to many relationship to have a value, with blank=True and null=True.
So, I would rewrite the save method like this:
def save(self, *args, **kwargs):
if self.id: # it means the object was already created, so has an id.
self.length = self.length_of_beam
self.total_quantity = self.total_concrete_quantity
super(BeamQuantity, self).save(*args, **kwargs)
Then when you want to create a BeanQuantity, do:
bean_quantity = BeanQuantity.objects.create(**fields_you_want_here)
bean_quantity.save()
The second line will run the code in the save method, because now the object has an id.

Related

Django models: which field to use for position/order of an object

My project is a basic scrum board with a model Column, of which there can be unlimited instances. Each column is displayed side by side, and I want the order of the columns to be customizable. My thought was to use an IntegerField to determine numerical position, but I'm struggling with how to set the max_value equal to the total number of columns since it varies by board.
Here's how I set it up:
class Column(models.Model):
name = models.CharField(max_length=25)
board = models.ForeignKey(Board, on_delete=models.CASCADE)
position = models.IntegerField(unique=True,min_value=1, max_value=???)
My problem was what to use as the max_value. I tried solving it by doing this:
class Column(models.Model):
max_value = Column.objects.get(board=board).count()
name = models.CharField(max_length=25)
position = models.IntegerField(unique=True,min_value=1, max_value=max_value)
I know this obviously won't work. Could I somehow use class Meta to set constraints instead, or what would be a good way to approach this?
Probably, you could redefine save() method.
class Column(models.Model):
name = models.CharField(max_length=25)
board = models.ForeignKey(Board, on_delete=models.CASCADE)
position = models.IntegerField(unique=True, min_value=1)
def save(self, **kwargs):
position_max_value = type(self).objects.get(board=self.board).count()
if self.position > position_max_value:
raise ValueError(...)
super().save(**kwargs)

Django CBV use Values from Modelform to Calculate other Model Fields

I am trying to make a warehouse management system with Django 3.2 based on this models:
class itemtype(models.Model):
item_id = models.IntegerField(primary_key=True)
item_name = models.CharField(max_length=100)
group_name = models.CharField(max_length=100)
category_name = models.CharField(max_length=100)
mass = models.FloatField()
volume = models.FloatField()
used_in_storage = models.BooleanField(default=False, null=True)
class Meta:
indexes = [
models.Index(fields=['item_id'])
]
def __str__(self):
return '{}, {}'.format(self.item_id, self.item_name)
class material_storage(models.Model):
storage_id = models.AutoField(primary_key=True)
material = models.ForeignKey(itemtype, on_delete=models.PROTECT)
amount_total = models.IntegerField(null=True)
price_avg = models.FloatField(null=True)
order_amount = models.IntegerField(null=True)
order_price = models.IntegerField(null=True)
timestamp = models.DateTimeField(default=timezone.now)
def __str__(self):
return '{}, {} avg.: {} ISK'.format(self.material, self.amount, self.price)
"itemtype" defines basically the possible objects which could be stored and "material_storage" shows what is in stock. I tried to combine the total amount of every item as well as the average price paid for it and the amount and price for a single order in the same database row. The idea is to get the last record for the chosen item/material when a new order happens, add the amount of that order and recalculate the avg price.
Theoretically this could be split up on two tables, but I don't see a reason to do so at the moment.
However, I am not able to figure out the actual function code to do the calculations. I am new to Django and therefor a bit overwhelmed by the complexity. I tried to use class based views and model forms, for the easy stuff that worked fine but now I am kind of lost.
Making a form just for adding new rows to that storage table was ok.
class NewAssetForm(forms.ModelForm):
material = MaterialChoiceField(models.itemtype.objects.filter(used_in_storage= True))
def __init__(self, *args, **kwargs):
super(NewAssetForm, self).__init__(*args, **kwargs)
self.fields['amount'].widget.attrs['min'] = 1
self.fields['price'].widget.attrs['min'] = 1
class Meta:
model = models.material_storage
fields = (
'material',
'amount',
'price'
)
widgets = {
'material': forms.Select(),
}
Same for the View to process it.
class NewItemView(FormView):
template_name = 'assetmanager/newasset.html'
form_class = forms.NewAssetForm
success_url = '/storage/current'
def form_valid(self, form):
return super().form_valid(form)
But now I am stuck. I thought this should be a fairly standard task, but I couldn't find a solution for it by now. The Idea was to put it in the form_valid function, take the material from the form to find the latest relevant record, add the new amount as well as calculate the new average price and save all together to the model. So far i only found a few examples comparable with my problem at all and I wasn't able to translate them to my setup, so maybe someone can give me a hint for a more successful search or provide me an example how to approach this topic.
thx in advance.
To modify the values of the form fields, you can override "clean" method and provide values to the form fields. Data can be accessed using "self.cleaned_data", it is a dictionary.
class NewAssetForm(ModelForm):
def clean(self):
super().clean()
# place code that retrieves existing data and calculate new values.
self.cleaned_data['price'] = 'New Value'
cleaned_data will be passed to "form_valid", there you can call the save function. "form.save()" will create a new row, make sure you are passing valid values to the views. Since you are accepting few fields in the form, make sure you have default values for the fields that are not included in the form object.
Thank you for your answer I found a solution by using the form_valid() method within the FormView. The majority of the code is used to create entries based on the existing entries or to check whether there are already entries for the same material.
class NewItemView(FormView):
template_name = 'assetmanager/newasset.html'
form_class = forms.NewAssetForm
success_url = '/storage/current'
def form_valid(self, form):
try:
# check if there is already a record for this material.
material_storage.objects.filter(material_id = form.cleaned_data['material'])[:1]
# getting total amount and average price values from latest entry with said material.
total_amount = material_storage.objects.values('amount_total').filter(material_id=form.cleaned_data['material']).order_by('-timestamp')[:1][0]['amount_total']
avg_price = material_storage.objects.values('price_avg').filter(material_id=form.cleaned_data['material']).order_by('-timestamp')[:1][0]['price_avg']
amount = form.cleaned_data['amount']
price = form.cleaned_data['price']
# calculating new total amount and average price based on old values and new entry.
form.instance.amount_total = total_amount + amount
form.instance.price_avg = ((avg_price * total_amount) + (price * amount)) / (total_amount + amount)
form.save()
except material_storage.DoesNotExist:
# if there is no entry for the chosen material yet, amount = total amount, price = average price.
form.instance.amount_total = form.cleaned_data['amount']
form.instance.price_avg = form.cleaned_data['price']
form.save()
return super().form_valid(form)
For now this solves my problem, however I don't know if the chosen location (form_valid()) makes sense - your answer suggests it would make more sense elsewhere.
Also, checking if an entry already exists for the material and selecting values from such an entry are pretty sure not very elegant and efficient. But as already mentioned, I am a beginner - I would be happy about any suggestions for improvement.
I am also not sure yet if this handles every probable special case which could appear...

Refer related model field in model layer

I am try to refer 'spot_price' of model 'Spot' in model 'Basis' in django model layer, How can I manage this?
I have designed view.py to automaticaly render the templates. so I am not able to modifty any view.py to choose data like 'models.B.objects.get().field'.
and more, str is set to indicate the date, so, in the django backstage admin, the 'spot' field display would be 'date' formate, how would be change to 'spot_price'?
model Spot
class Spot(models.Model):
date = models.DateField(primary_key=True)
spot_price = models.FloatField(blank=True, null=True)
def __str__(self):
return str(self.date) if self.date else ''
need to refer the model Spot'spot_price by date, cause date is unique but spot_price is not
class Basis(models.Model):
date = models.DateField(primary_key=True)
major_future_contract_close_price = models.FloatField(blank=True)
spot = models.OneToOneField(Spot, on_delete=models.CASCADE)
basis = models.FloatField(default=calculate_basis)
def __str__(self):
return str(self.date) if self.date else ''
def calculate_basis(self):
return abs(self.major_future_contract_close_price -
self.spot.spot_price)
I expect the Basis.query.data would to like 'date: 2019-04-25, major_future_contract_close_price: 100.0, spot: 96.5, basis: 3.5'
You can't use class method as default, because it requires self, which is not existing when you are still creating the object.
If you need to have it stored in field (database), override default save() method or use signals to modify the basis field once your object is created. Also note that you have to recalculate basis every time close_price or spot_price changes, as the value is just written in database.
Probably better solution would be to use #property so for anyone that will be using you model it will look like a field, but it will dynamically calculate value based on current data.
For example, I'm overriding save() to calculate the basis field. I set it as editable=False, which means it won't appear in forms by default (and you will not be able to see it in admin!). You can safely remove that part if you want to just look at the values.
Additionally I add basis_as_property property.
class Basis(models.Model):
date = models.DateField(primary_key=True)
major_future_contract_close_price = models.FloatField(blank=True)
spot = models.OneToOneField(Spot, on_delete=models.CASCADE)
basis = models.FloatField(editable=False, blank=True)
#property
def basis_as_property(self):
return '%s' % (self.calculate_basis())
def __str__(self):
return str(self.date) if self.date else ''
def save(self, *args, **kwargs):
if not self.basis:
self.basis = self.calculate_basis()
super(Basis, self).save(*args, **kwargs)
def calculate_basis(self):
return abs(self.major_future_contract_close_price - self.spot.spot_price)
As for Spot str repr, I don't think it's possible to change it based on where it is referenced. If you want to use spot_price, you can just use: return str(self.spot_price) if self.spot_price else str(self.date)

Django: Where to store a lot of methods called on model.save()?

I am using Django 1.9. I have a model that represents the number of patients at a doctor's practice in a given month, broken down by age and sex:
class PracticeList(models.Model):
practice = models.ForeignKey(Practice)
date = models.DateField()
male_0_4 = models.IntegerField()
female_0_4 = models.IntegerField()
male_5_14 = models.IntegerField()
female_5_14 = models.IntegerField()
... etc
total_list_size = models.IntegerField()
prescribing_needs = JSONField(null=True, blank=True)
I use the value of the integer fields to drive various measures adjusted for age and sex. There are a large and unpredictable number of these, hence using a JSONField for prescribing_needs. I initially calculated and set these on the model's save method:
def save(self, *args, **kwargs):
self.total_list_size = self.male_0_4 + self.female_0_4 + ....
antibiotics = 1.2 * self.male_0_4 + 1.1 * self.female_0_4 + ...
# plus 40-50 other calculations
self.prescribing_needs = {
'antibiotics': antibiotics ...
}
super(PracticeList, self).save(*args, **kwargs)
This worked, but made my models file unmanageably long. So my question is quite simple: What is the Django Way to split out the method to calculate all these measures on save?
Right now I've simply created a new file called model_calculations.py in the same directory as models.py:
def set_prescribing_needs(c):
antibiotics = 1.1 * c.male_0_4 + 1.1 * female_0_4 ...
prescribing_needs = {
'antibiotics': antibiotics
}
return prescribing_needs
And I just import this file into models.py and do:
def save(self, *args, **kwargs):
self.prescribing_needs = model_calculations.set_prescribing_needs(self)
super(PracticeList, self).save(*args, **kwargs)
Is this OK, or is there a more Djangoish place to store these methods?
That is an OK way to do it and is the Django way of doing it. There are two things I would look for.
When you have some functions that are used in multiple apps, put those functions in a main/model_calculations.py, core/model_calculations.py, or whatever app you have that is shared across your whole project.
If you find that some of these functions are used outside of your models, I would put them in a utils.py file.
As long as these functions are used just in a single app, and only used in models, where you're currently storing them is fine.

Django. Doing complex calculations in database

Here's part of my model file :
class Metric(models.Model):
Team = models.ForeignKey(Team)
metric_name = models.CharField(max_length = 40)
def __unicode__(self):
return self.metric_name
class Members(models.Model):
Metric = models.ForeignKey(Metric, through="Calculate")
member_name = models.CharField(max_length = 40, null=True, blank=True)
week_one = models.IntegerField(null=True, blank=True)
week_two = models.IntegerField(null=True, blank=True)
week_three = models.IntegerField(null=True, blank=True)
week_four = models.IntegerField(null=True, blank=True)
total = models.IntegerField(null=True, blank=True)
def __unicode__(self):
return self.member_ID
def save(self, *args, **kwargs):
self.total = int(self.week_one)+int(self.week_two)+int(self.week_three)+int(self.week_four)
super(Members, self).save(*args, **kwargs) # Call the "real" save() method.
Now what I want to do is. I want to calculate the number of members per metric the aggregate total of all the members in a metric and the highest total among all the members in a metric.
I can't figure out a way to do this in Django.
I want to make these calculations and store them in the database.
Can anyone please help me out with this.
Thanks
If you wish to "memoize" these results, there are at least two paths that you could follow:
A per-row post-save trigger on the Members table that updates "members_count", "members_total" and "members_max" fields in the Metric table.
The challenge with this is in maintaining the trigger creation DDL alongside the rest of your code and applying it automatically whenever the models are re-created or altered.
The Django ORM does not make this especially easy. The commonest migration tool ( south ) also doesn't go out of its way to make this easy. Also note that this solution will be specific to one RDBMS and that some RDBMSs may not support this.
You could create "synthetic" fields in your Metric model then use a post-save signal handler to update them whenever a Member is added or changed.
class Metric(models.Model):
Team = models.ForeignKey(Team)
metric_name = models.CharField(max_length = 40)
members_count = models.IntegerField()
members_max = models.IntegerField()
members_total = models.IntegerField()
def __unicode__(self):
return self.metric_name
# signal handling
from django.db.models import signals
from django.dispatch import dispatcher
def members_post_save(sender, instance, signal, *args, **kwargs):
# Update the count, max, total fields
met = sender.Metric # sender is an instance of Members
metric.members_count = Members.objects.filter(Metric=met).count()
# more code here to do the average etc;
dispatcher.connect(members_post_save, signal=signals.post_save, sender=Members)
The django signals documentation here can be of use.
Caveats
While this sort of approach could be made to achieve your stated goal, it is brittle. You need to have test coverage that ensures that this signal handler always fires, even after you've done some refactoring of your code.
I would also consider using "related objects" queries [ documented at https://docs.djangoproject.com/en/dev/topics/db/queries/#related-objects ] eg, assuming we have a "me" instance of Metric
>> members_count = me.members_set.count()
>> # aggregation queries for the total and max
If these aggregates are not used very often, path #2 could be a viable and more maintainable option.

Categories