Action when a foreign key is being changed on django - django

The question in general is about finding the modification of a foreign key of a model and call some function of the related model.
Assume I have two model class:
class Discount(models.Model):
def use(self, sell_item):
if self.max_price:
self.max_price -= sell_item.net()
if self.max_count:
self.max_count -= sell_item.amount
self.save()
def deuse(self, sell_item):
if self.max_price:
self.max_price += sell_item.net()
if self.max_count:
self.max_count += sell_item.amount
self.save()
max_price = models.PositiveIntegerField(blank=True,
null=True)
max_count = models.PositiveIntegerField(blank=True,
null=True)
amount = models.PositiveIntegerField(blank=False,
null=False)
class SellItem(models.Model):
def net(self):
price = self.amount * self.price
if self.discount:
price -= self.discount.amount * price / 100
return price * (1 + self.tax / 100)
amount = models.PositiveIntegerField(balnk=False,
null=False)
price = models.PositiveIntegerField(blank=False,
null=False)
tax = models.PositiveIntegerFeidl(blank=False,
null=False)
discount = models.ForeignKey(Discount,
blank=True,
null=True)
Now I want to execute use function whenever a discount add to an item and deuse it whenever it is being removed from an item. I found a post about it and to do that I write below code for sell item:
def __init__(self, *args, **kwargs):
self.dirty = False
self.pre_states = []
self.new_states = []
super(SellItem, self).__init__(*args, **kwargs)
def __setattr__(self, name, value):
if name == 'discount':
if hasattr(self, name):
pre_discount = self.discount
if pre_discount != value:
self.dirty = True
if pre_discount:
self.pre_states = ['pre_discount']
self.pre_discount = pre_discount
if value:
self.new_states = ['discount']
object.__setattr__(self, name, value)
def save(self, *args, **kwargs):
super(SellItem, self).save(*args, **kwargs)
if self.dirty:
if 'pre_discount' in self.pre_states:
self.pre_discount.deuse(self)
if 'discount' in self.new_states:
self.discount.use(self)
But it is not enough, because basically django would not fetch a foreign key when a new class is constructed, it instead just fill the _id item for it and whenever you need that it would fetch it from database, if I check for modification of discount_id instead of discount based on the order of setting of member values I may miss the previous discount because I have just current and previous discount_id not discount.
I know that it could possible implement with checking all of cases but I think after all I depend on django implementation of the behavior of database fetching which could be changed further.
I think there must be a proper and easier solution for just knowing the modification of a foreign key, I know there is some packages for storing history of modification but they are too much for my simple request.

Related

Django Many to Many Field Set in Model Save Method

I am trying to override the save method in a model with logic to update a couple of many to many fields. Using print statements I can see values updating as expected but the values are not persisted after save.
In the below model the change_access_flag is changing as expected with a signal, the prints are executing with the appropriate values, but the allowed_departments and allowed_communities fields are not updating with the printed values.
Model
class Person(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
full_name = models.CharField(null=True, blank=True, max_length=50)
payroll_id = models.CharField(null=True, max_length=20)
position = models.ForeignKey(Position, null=True, on_delete=models.SET_NULL)
primary_community = models.ForeignKey(Community, null=True, on_delete=models.CASCADE, related_name="primary_community")
region = models.CharField(max_length=2, choices=RegionChoices.choices, blank=True, null=True)
allowed_communities = models.ManyToManyField(Community, blank=True, related_name="allowed_community")
allowed_departments = models.ManyToManyField(Department, blank=True)
access_change_flag = models.BooleanField(default=False)
def __str__(self):
return f'{self.user.first_name} {self.user.last_name}'
class Meta:
verbose_name_plural = "People"
ordering = ['position__position_code', 'user__last_name', 'full_name']
def save(self, *args, **kwargs):
#Set Full Name field
if self.user.last_name:
self.full_name = f'{self.user.first_name} {self.user.last_name}'
super().save(*args, **kwargs)
#Change flag set in signals, set for events that require updating access settings
if self.access_change_flag:
self.access_change_flag = False
#Allowed community access
access_level = self.position.location_access_level
self.allowed_communities.clear()
if access_level == 'R':
if self.primary_community.community_name == '#':
region = self.region
else:
region = self.primary_community.region
if region is not None:
communities = Community.objects.filter(region=region)
self.allowed_communities.set(communities)
self.allowed_communities.add(self.primary_community)
elif access_level == 'A':
communities = Community.objects.filter(active=True)
self.allowed_communities.set(communities)
else:
communities = self.primary_community
self.allowed_communities.add(communities)
print(self.allowed_communities.all())
#Allowed department access
dept_access = self.position.department_only_access
if dept_access:
depts = [self.position.department]
else:
depts = Department.objects.filter(active=True)
self.allowed_departments.set(depts)
print(self.allowed_departments.all())
super().save(*args, **kwargs)
I have tried variations of set, clear, add, moving the super.save() around, and placing the logic in a signal but nothing seems to work. I have tested initiating save from both a model form through a view and admin.
Let me answer in quotes. You can find the source in this section.
If you wish to update a field value in the save() method, you may also
want to have this field added to the update_fields keyword argument.
This will ensure the field is saved when update_fields is specified.
Also read here
Specifying update_fields will force an update.
So try to call the super().save(*args, **kwargs) method at the end with defining the argument update_fields. This will force the update of your model regarding the specified fields.
Let me know how it goes.

Querying other Model in class-based view produces error

I have two tables, in one of which the possible items with their properties are recorded, in the other the stock levels of these respective items are recorded.
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()
packaged_volume = models.FloatField(null=True)
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)
amount = models.IntegerField(null=True)
price = models.FloatField(null=True)
timestamp = models.DateTimeField(default=timezone.now)
def __str__(self):
return '{}, {} avg.: {} ISK'.format(self.material, self.amount, self.price)
I have a ModelForm based on the table material_storage, in which a checkbox indicates whether transport costs should be included or not.
In the form_valid() method of this ModelForm class the calculations are performed. To do so, I have to retrieve the volume per unit of the given item to use it for my transport cost calculations. Trying to geht that value the way shown below leads to an error I don't really understand.
class MaterialChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return obj.item_name
class NewAssetForm(forms.ModelForm):
material = MaterialChoiceField(models.itemtype.objects.filter(used_in_storage= True))
needs_transport = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
super(NewAssetForm, self).__init__(*args, **kwargs)
self.fields['amount'].widget.attrs['min'] = 1
self.fields['price'].widget.attrs['min'] = 0.00
class Meta:
model = models.material_storage
fields = (
'material',
'amount',
'price',
)
widgets = {
'material': forms.Select(),
}
class NewItemView(FormView):
template_name = 'assetmanager/newasset.html'
form_class = forms.NewAssetForm
success_url = '/storage/current'
def form_valid(self, form):
unit_volume = itemtype.objects.values('packaged_volume').filter(item_id=form.cleaned_data['material'])[0]['packaged_volume']
I believe that this has something to do with querying a different model than specified in the form, but I don't understand what exactly is the problem. Especially the fact, that running the exact same query in the django shell returns the correct value does not really help to understand what is going wrong here. Could somebody please tell me how to get the desired value the correct way?
Change last line from:
unit_volume = itemtype.objects.values('packaged_volume').filter(item_id=form.cleaned_data['material'])[0]['packaged_volume']
to:
unit_volume = itemtype.objects.values('packaged_volume').filter(item_id=form.cleaned_data['material'].item_id)[0]['packaged_volume']
The error says, you are giving Item instance to the query, where is item_id asked.

Django - Adding up values in an object's many to many field?

I have 2 classes
class Service(models.Model):
service_name = models.CharField(max_length=15, blank=False)
service_time = models.IntegerField(blank=False)
class Appointment(models.Model):
name = models.CharField(max_length=15)
service_selected = models.ManytoManyField(Service, blank=True)
total_time = models.IntegerField(null=True, blank=True)
What Im trying to do is that after a user has selected the services and created an appointment, each of the services' service_time will be added up to equal total_time
I know I have to use something along the lines of
#in class Appointment
def save(self, *args, **kwargs):
self.total_time += self.service_selected.service_time #it needs to add up all of the service_time from each service selected but that's where Im lost
super(Appointment, self).save(*args, **kwargs)
But I don't know how to get the service_time value from each of the service_name that were selected, or how to query for every service_selected chosen in that appointment's object so that I can add up those values.
edit:
Had to change
total_time = models.IntegerField(blank=False)
to
total_time = models.IntegerField(blank=False, null=False, default=0)
for the answer to work
You can do it as follows:
#in class Appointment
def save(self, *args, **kwargs):
self.total_time += Service.objects.all().aggregate(total_time=Sum('service_time'))['total_time']
# You will have to run a query to get the data and aggregate according to that. You may change the query according to your needs.
super(Appointment, self).save(*args, **kwargs)

Calculate and save the total of an invoice in django

When I add an invoice, the Total is always 0 but when I update without any changes, it's updated with the totalsubtotals(). I understand that there are many calculations and in my case, the total calculation is done before the subtotals. Any recommendations.
class Invoice(models.Model):
date = models.DateField(default=timezone.now)
client = models.ForeignKey('Client',on_delete=models.PROTECT)
total = models.DecimalField(default=0, max_digits=20, decimal_places=2)
def totalsubtotals(self):
items = self.invoiceitem_set.all()
total = 0
for item in items:
total += item.subtotal
return total
def save(self, *args, **kwargs):
self.total = self.totalsubtotals()
super(Invoice, self).save(*args, **kwargs)
class InvoiceItem(models.Model):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.PROTECT)
price = models.DecimalField(max_digits=20, decimal_places=2)
quantity = models.DecimalField(max_digits=20, decimal_places=2)
subtotal = models.DecimalField(default=0, max_digits=20, decimal_places=2)
def save(self, *args, **kwargs):
self.subtotal = self.price * self.quantity
super(InvoiceItem, self).save(*args, **kwargs)
It looks to me like your "default = 0" in your InvoiceItem model under subtotal is what's causing the problem, if there is any error with price or quantity the default value is stored, returning 0 to your Invoice model.
I find that default values also make debugging a lot harder so I try to only use them where values are optional, in the case of an invoice, you can't order no quantity of products and you can't have no price either (0 is a number) errors in the input would set the value in the DB to Null (or None in the case of Python) and then your default sets the subtotal to 0.
Removing the default would cause errors when you try to input your values and you can better track down where the issue is based on your error messages.
Alternatively, in your save function for InvoiceItem, you can try...
if self.price && self.quantity: (check that they're not Null/None)
self.subtotal = self.price * self.quantity
else:
raise ValueError('Incorrect values in price or subtotal')

Django save() method not producing desired output

I am working on patient databases. Two related class, one which stores details of patients, and the other which is just to track total numbers of different types of patients. The tracker class is as follow :
class Case (models.Model):
id = models.AutoField(primary_key=True)
time_stamp = models.DateTimeField(null=True, auto_now_add=True)
is_dicom = models.BooleanField(null=False, blank=True)
PatientId = models.IntegerField(null=True, blank=True, db_column="patientid")
def __unicode__(self):
return "%s" % self.id
This has a 'PatientId' field which refers to two different classes (hence not a foreign key to any one class) and just stores the pk of the referring class as integer. The referring classesa are Patient and PatientJpeg which run similar code on save() :
class Patient(models.Model):
id = models.AutoField(primary_key=True, db_column='pk')
case_id = models.OneToOneField(Case, null=True, blank=True, db_column='case_id')
pat_birthdate = models.CharField(max_length=160, blank=True)
pat_sex = models.CharField(max_length=160, blank=True)
created_time = models.DateTimeField(null=True, auto_now_add=True)
updated_time = models.DateTimeField(null=True, auto_now_add=True)
class Meta:
db_table = 'patient'
def __unicode__(self):
return u"%s" % self.id
def save(self):
if not(self.case_id):
x = 1
while (x < Case.objects.count() + 1):
if not Case.objects.filter(pk=x).exists():
break
else:
x += 1
newCase = Case(x)
newCase.is_dicom = True
newCase.PatientId = self.id
newCase.save()
self.case_id = newCase
super(Patient, self).save()
class PatientJpeg (models.Model):
id = models.AutoField(primary_key=True, db_column='pk')
case_id = models.OneToOneField(Case, null=True, blank=True, db_column='case_id')
pat_age = models.CharField(max_length=160, null=True, blank=True)
pat_sex = models.CharField(max_length=160, null=True, blank=True)
def __unicode__(self):
return u"%s" % self.id
def jpeg_paths(self):
array = []
for x in self.jpeg_set.all():
array.append(x.image.path)
return array
class Meta:
db_table = 'patient_jpeg'
def save(self):
if not(self.case_id):
x = 1
while (x < Case.objects.count() + 1):
if not Case.objects.filter(pk=x).exists():
break
else:
x += 1
newCase = Case(x)
newCase.is_dicom = False
newCase.PatientId = self.id
newCase.save()
self.case_id = newCase
super(PatientJpeg, self).save()
The problem is that when I save Patient or PatientJpeg, PatientId field (integer field in Case class) remains null. I have replicated this code in shell and it behaves properly, I dont know why it doesnt behave inside django.
Thanks for looking at the code
This code is, to be polite, pretty horrible. It's doing an insane amount of database access each time you create a new Patient - imagine you had 1000 Cases, just saving a new Patient would result in over 1000 database calls.
What's strange is that I can't understand why you need it. The case ID is an arbitrary number, so there doesn't seem to be any particular reason why you need to iterate through existing case IDs to find a blank one. This is especially so given that the ID is an AutoField, so will be allocated by the database anyway. Just get rid of all that and simply create a new case.
The other problem of course is that self.id does not exist when you first save the Patient. You need to ensure that it is saved before allocating the Case.
def save(self, *args, **kwargs):
if not(self.case_id):
if not self.id:
super(Patient, self).save(*args, **kwargs)
newCase = Case()
newCase.is_dicom = True
newCase.PatientId = self.id
newCase.save()
self.case_id = newCase
super(Patient, self).save(*args, **kwargs)
Other pointers: don't call your one-to-one relationship case_id: it's a case object, not an ID, so just call it case. Also, you should really create an abstract class as the parent of both Patient and PatientJpeg, and put the custom save logic there. Finally, your jpeg_paths method could be replaced by a list comprehension.
The time you call newCase.save(), your PatientJpeg record has not been saved to the database. AutoFields in django are evaluated when you try to save them to database, because id must be genertated by the databse. So when you call newCase.save(), self.id in newCase.PatientId = self.id is None
You must save your data after you call super(PatientJpeg, self).save() so self.id will have a valid not None value
Also I suggest you to use ContentTypes Framework if your ForeignKey might be elated to more than one table.
Before creating Case you need to save Patient, so i think the save method should be like:
def save(self):
if not self.id:
super(PatientJpeg, self).save()
if not self.case_id:
x = 1
while (x < Case.objects.count() + 1):
if not Case.objects.filter(pk=x).exists():
break
else:
x += 1
newCase = Case(x)
newCase.is_dicom = False
newCase.PatientId = self.id
newCase.save()
self.case_id = newCase
super(PatientJpeg, self).save()
Look also at django post_save signal