check what field value changed in django model - python

I parse data from a restful API call using django-rest-framework ModelSerializer.
Here is the code:
url = "API URL HERE"
r = requests.get(url)
json = r.json()
serializer = myModelSerializer(data=json, many=True)
if serializer.is_valid():
serializer.save()
Here is the modelSerializer:
class myModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
MYModel:
class MyModel(models.Model):
City = models.NullBooleanField(default=False, null=True)
Status = models.CharField(max_length=100, null=True)
stateName = models.CharField(max_length=50)
marketingName = models.TextField(null=True)
county = models.CharField(max_length=200, null=True)
My problem is I need to find out what field value changed from the last time I called the restful api and updated data. OR If there is any new records. How do I achieve this?

First, you could add a column to your MyModel:
updated = models.DateTimeField(auto_now=True)
This will update whenever an instance is changed. If you filter on this field in your queryset, you can determine what rows changed and if there are new rows.
Finding out what field changed is harder, but here is an idea--add a string field to the model and write a custom save method for your model, like this:
class MyModel(models.Model):
City = models.NullBooleanField(default=False, null=True)
Status = models.CharField(max_length=100, null=True)
stateName = models.CharField(max_length=50)
marketingName = models.TextField(null=True)
county = models.CharField(max_length=200, null=True)
updated = models.DateTimeField(auto_now=True)
updated_fields = models.CharField(max_length=200, null=True)
def save(self, *args, **kwargs):
if not self.pk: # if this is new, just save
super(MyModel, self).save(*args, **kwargs)
else:
# get the original
old = MyModel.objects.get(id=self.pk)
# make a list of changed fields
changed_fields = []
for field in self._meta.get_all_field_names():
if getattr(self, field, None) != getattr(old, field, None):
if field not in ['updated', 'updated_fields']:
changed_fields.append(old)
# make a comma separated string
self.updated_fields = ','.join(changed_fields)
super(MyModel, self).save(*args, **kwargs)
Now the updated_fields column will contain the set of fields that were last updated.

Related

<PYTHON DRF> Why am I getting only one value instead of list from request.data?

I have little problem with accessing to all values from request.data.
Here my code and screens:
My viewset:
class LorebookViewset(ModelViewSet):
queryset = Lorebook.objects.all()
serializer_class = LorebookSerializer
permission_classes = (
permissions.IsAuthenticated,
)
def perform_create(self, serializer):
data = self.request.data
print(data)
print(data['teams'])
Serializer:
class LorebookSerializer(serializers.ModelSerializer):
teams = serializers.StringRelatedField(many=True, read_only=True)
categories = serializers.StringRelatedField(many=True, read_only=True)
author = serializers.StringRelatedField(read_only=True)
class Meta:
model = Lorebook
fields = '__all__'
Model:
class Lorebook(models.Model):
title = models.CharField(max_length=50, unique=True)
cover = models.FileField(default='lorebooks/cover.jpg', null=True)
thumbnail = models.FileField(default='lorebooks/thumb.jpg', null=True)
banner = models.FileField(default='lorebooks/banner.jpg', null=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
tags = ArrayField(models.TextField(), default=list)
categories = models.ManyToManyField(Category)
teams = models.ManyToManyField(Team)
date_created = models.DateTimeField(auto_now_add=True)
trailer = models.TextField()
is_read = models.BooleanField(default=False)
def __str__(self):
return self.title
Data in Postman:
Print in perform_create in viewset:
Why am I getting only last value from teams instead of list?
This is actually what you're supposed to get as per the spec of a QueryDict even if it's not what you as a person who is used to interacting with Python dicts would expect. The spec returns the last value for any key whose value is a list.
https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.QueryDict.update
I believe the idea here is to prevent code that kind of naively assumes that you're asking for/getting a single item from blowing up when it gets a list of items instead.
If you want to get the whole list:
def perform_create(self, serializer):
data = self.request.data
print(data)
print(data.getlist('teams')
should do the trick.

Update records depending on fields - Django

I want to override the save method of a model in Django.
Consider this model:
class OtherClass(models.Model):
name = models.CharField(max_length=255, blank=True, null=True)
class MyClass(models.Model):
admin = models.ForeignKey(OtherClass, on_delete=models.CASCADE)
myfield1 = models.CharField(max_length=128)
myfield2 = models.CharField(max_length=128)
myfield3 = models.CharField(max_length=128, blank=True, null=True)
myfield4 = models.CharField(max_length=128, blank=True, null=True)
myfield5 = models.TextField(blank=True)
What I want, is to update the model only if myfield5 is present in whatever form or view the user is filling. This is the current save method I have:
def save(*args, **kwargs):
fieldgroup = f"{self.myfield2}\n{self.myfield3}"
self.myfield5 = f"{fieldgroup}\n{self.field1} {self.field4} {self.field5}"
super().save(*args, **kwargs)
Any ideas?
Maybe it would be better to achieve this on a form rather than directly into the model?
You can check self.pk to determine whether the object was already previously saved or not (the first time self.pk is None).
So in combination with self.field5, you can do this:
if self.pk and self.field5:
self.field5 = ... # update field5 as you need
super().save(...)
elif not self.pk:
super().save(...)
# no saving if self.pk and not self.fields
You can use required=True in your Model field or Form field

how to override django save method to update some field dynamically?

Here i have two models.In these models i want to make the value of amount_to_pay dynamic in Ledger model.For Example i have two different forms for these two models and while saving expense form if the user select the payment_option which comes from ledger model and gives some value for the amount_to_pay field then if only ledger.id and expense.payment_option_id are same then the value of amount_to_pay in ledger model should be replaced with that value.how can i do it ?
models.py
class Expense(models.Model):
pay_from = models.CharField(max_length=200)
payment_option = models.ForeignKey('Ledger', on_delete=models.CASCADE)
amount_to_pay = models.IntegerField(default=0)
expense_date = models.DateField(default=datetime.date.today)
expense_type = models.ForeignKey(ExpenseType, on_delete=models.CASCADE)
note = models.TextField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
slug = AutoSlugField(unique_with='id', populate_from='expense_type')
def get_amount_to_pay(self):
return self.amount_to_pay
class Ledger(models.Model):
name = models.CharField(max_length=200)
account_number = models.CharField(max_length=250, unique=True)
account_type = models.CharField(max_length=200)
opening_balance = models.IntegerField(default=0)
amount_to_pay = models.IntegerField(default=0, blank=True, null=True)
current_balance = models.IntegerField(default=0, blank=True, null=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
slug = AutoSlugField(unique_with='id', populate_from='name')
def save(self, *args, **kwargs):
self.amount_to_pay = Expense.get_amount_to_pay(self)
# here how can i save the amount_to_pay from expense form if the ledger.id and expense.payment_option.id matches??
#i got stuck here.
self.current_balance = self.opening_balance - self.amount_to_pay
super(Ledger, self).save(*args, **kwargs)
Solution One:
I think instead of changing in Ledger model, you should change in Expense model, like this:
class Expense(models.Model):
...
def save(self, *args, **kwargs):
self.payment_option.amount_to_pay = self.payment_option.amount_to_pay + self.amount_to_pay
self.payment_option.save()
super(Expense, self).save(*args, **kwargs)
Solution Two:
But to be honest, Solution One does not seem good to me. Reason is that you are saving same data in 2 places(in both expense and ledger). Instead, it should be once, then the amount_to_pay value in Ledger should be calculated dynamically. Like this:
from django.db.models import Sum
class Ledger(...):
#property
def amount_to_pay(self):
# I am using a property method to show the amount_to_pay value.
# FYI: in this way, you need to remove amount_to_pay field from Ledger model
return self.opening_balance - self.expense_set.all().aggregate(a_sum=Sum('amount_to_pay')).get('a_sum', 0)
In that way, with each ledger, the value amount_to_pay will be dynamically calculated at runtime. For example:
for l in Ledger.objects.all():
l.amount_to_pay
Solution Three:
If you are wary of making DB hits with each l.amount_to_pay(as it calculates amount_to_pay from DB dynamically) from previous solution, then you can always annotate the value. Like this:
For this solution, you need to change your Expense model and add a related_name:
class Expense(models.Model):
pay_from = models.CharField(max_length=200)
payment_option = models.ForeignKey('Ledger', on_delete=models.CASCADE, related_name='expenses')
Then use that related_name in query like this(FYI: You can't keep def amount_to_pay(...) method in Ledger model for the following usage example):
from django.db.models import Sum, F, ExpressionWrapper, IntegerField
ledgers = Ledger.objects.all().annotate(expense_sum=Sum('expenses__amount_to_pay')).annotate(amount_to_pay=ExpressionWrapper(F('opening_balance') - F('expense_sum'), output_field=IntegerField()))
# usage one
for l in ledgers:
l.amount_to_pay
# usage two
ledgers.values('amount_to_pay')
Best thing is you override the call in save method any way you have foreginkey.
def save(self, *args, **kwargs):
#self.date_created = timezone.now()
# YOUR LOGIC HERE
super(YOUR_OVERRIDING_MODEL , self).save(*args, **kwargs
check if Ledger have child and then update it:
class Expense(models.Model):
def save(self, *args, **kwargs):
self.payment_option.amount_to_pay = self.payment_option.amount_to_pay + self.amount_to_pay
self.payment_option.save()
super(Expense, self).save(*args, **kwargs)
Override save method and update some field dynamically in django
For my case i wanted to update done column in answer model dynamically when i comment on an answer it should return true instead of False. So in my example i am checking for whether that column done which is boolean is False and if it's False,i change it's status to True using the answer object from Comment and then save the answer object, see more about update vs insert fields https://docs.djangoproject.com/en/3.2/ref/models/instances/
class Answer(models.Model):
user = models.ForeignKey("User", on_delete=models.CASCADE, blank=True, null=True)
username = models.CharField(max_length=250)
codeAnswer = models.CharField(max_length=250)
girAnswer = models.CharField(max_length=250)
correct = models.BooleanField(default=False)
firebaseToken = models.CharField(max_length=250)
done = models.BooleanField(default=False)
created_at = models.DateTimeField()
updated_at = models.DateTimeField(auto_now=True)
def __unicode__(self):
return "{0}: {1}".format(self.username, self.value)
def __str__(self):
return "%s" % (self.codeAnswer)
# So here i override the save method in the Comment model to do what i want as below.
class Comment(models.Model):
answer = models.ForeignKey("Answer", on_delete=models.CASCADE, blank=False , related_name="comments")
writerName = models.CharField(max_length=250)
content = models.CharField(max_length=250)
deleted = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return "%s" % (self.writerName)
def save(self, *args, **kwargs):
if self.answer.done is False:
self.answer.done = True
self.answer.save()
super(Comment, self).save(*args, **kwargs)
Hope it answers the question above thanks

I am trying to get all updated value in a model and send an email to users about the changes in django

i have a model A which has a many to one relationship with another model B,i want to be able to get send an email notification to emails which will be queried from model B any time values change in model A how can i do that in django?
class ModelA(models.Model):
"""
model to store informations about the ModelA
"""
name = models.CharField(max_length=15, blank=True, unique=True)
creator = models.ForeignKey(AppUser, blank=True, null=True, on_delete=models.CASCADE)
periodicity = models.CharField(max_length=10, blank=False, choices=PERIODICITY_CHOICES)
begin_date = models.DateField(blank=False)
end_date = models.DateField(blank=True, null=True)
subscription_fee = models.DecimalField(max_digits=5, decimal_places=2, blank=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
public = models.BooleanField(default=False)
maximum_sub = models.PositiveIntegerField(blank=True, null=True, validators=[MinValueValidator(2)])
def save(self):
emails = []
body = """
Hello <user first name or nothing>,
This is to notify you that ModelA you're subscribed to has been updated. Here is what has changed:
<field_name_1>: <current_value>
<field_name_2>: <current_value>
etc...
Thank you
"""
if self.pk is not None:
modelA_id = ModelA.objects.get(pk=self.pk)
modelA_members = ModelB.objects.filter(pk=modelA_ID)
emails = [i for i in modelA_members.email]
send_mail('ModelA change Notification', body, "noreply#example.com", [emails])
super(ModelB, self).save() #
def __unicode__(self):
return u'{0}'.format(self.name)
class ModelB(models.Model):
"""
defineModelB
"""
modelA = models.ForeignKey(ModelA, on_delete=models.CASCADE, blank=False)
user = models.ForeignKey(AppUser, on_delete=models.CASCADE, blank=False, )
cash_out_date = models.DateField(blank=True, null=True)
Okay, so this is one way to solve your problem of detecting the "changed fields" and autosending the email:
Here's how it works. The mixin overrides the class init and saves the existing values in an attribute called self.__initial. Your new values are in self._dict and the difference between the two are the changed values, this is captured by the property diff. We can leverage diff to get a dictionary of {key: (old_value, new_value)}, which you can then use in your templates
class ModelA(ModelDiffMixin, models.Model):
pass # Your code goes here
def save(self):
changed_fields = self.diff
for key, old, new in changed_fields.items():
# Do something here with the values
emails = []
body = """
Hello <user first name or nothing>,
This is to notify you that ModelA you're subscribed to has been updated. Here is what has changed:
<field_name_1>: <current_value>
<field_name_2>: <current_value>
etc...
Thank you
"""
if self.pk is not None:
modelA_id = ModelA.objects.get(pk=self.pk)
modelA_members = ModelB.objects.filter(pk=modelA_ID)
emails = [i for i in modelA_members.email]
send_mail('ModelA change Notification', body, "noreply#example.com", [emails])
super(ModelB, self).save() #
class ModelDiffMixin(object): # Credit to ivanperelivskiy here: https://stackoverflow.com/questions/1355150/django-when-saving-how-can-you-check-if-a-field-has-changed
"""
A model mixin that tracks model fields' values and provide some useful api
to know what fields have been changed.
"""
def __init__(self, *args, **kwargs):
super(ModelDiffMixin, self).__init__(*args, **kwargs)
self.__initial = self._dict
#property
def diff(self):
d1 = self.__initial
d2 = self._dict
diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
return dict(diffs)
#property
def has_changed(self):
return bool(self.diff)
#property
def changed_fields(self):
return self.diff.keys()
def get_field_diff(self, field_name):
"""
Returns a diff for field if it's changed and None otherwise.
"""
return self.diff.get(field_name, None)
def save(self, *args, **kwargs):
"""
Saves model and set initial state.
"""
super(ModelDiffMixin, self).save(*args, **kwargs)
self.__initial = self._dict
#property
def _dict(self):
return model_to_dict(self, fields=[field.name for field in
self._meta.fields])
You have done well by overriding the save method, to access all modelB linked to modelA instance, you have to use the reverse query related_name
Class ModelA(models.Model):
def save(self,*args,**kwargs):
if self.pk is not None:
body = """
Hello <user first name or nothing>,
This is to notify you that ModelA you're subscribed to has been updated. Here is what has changed:
<field_name_1>: <current_value>
<field_name_2>: <current_value>
etc...
Thank you
"""
modelB_members = self.modelb_set.all()
emails = [i.user.email for i in modelB_members]
send_mail('ModelA Change Notification', body, "noreply#example.com", [emails])
super(ModelA, self).save(*args,**kwargs) #
class ModelB(models.Model):
modelA = models.ForeignKey(ModelA, on_delete=models.CASCADE, blank=False)
user = models.ForeignKey(AppUser, on_delete=models.CASCADE, blank=False, )
cash_out_date = models.DateField(blank=True, null=True)

Django overriding model save method sets unspecified fields to NULL

Explanation
I have an extension of my model in eiysTblModels because we are using inspectdb option of django manage.py. Since it overwrites the models.py,we do not touch models.py, instead write our extensions to eiysTblModels.
Problem
Anyway, when I call edit_group function, it sets the slug and dates correctly as specified but it overwrites the other fields such as is_active, isapproved etc to NULL, which are initially set to 1.
vieys.py
def edit_group(request,group_id):
groupinfo = request.POST
group = eiysTblGroup(id = group_id )
group.name = groupinfo.get('group-name','')
group.save()
eiysTblModels.py
class eiysTblGroup(TblGroup):
class Meta:
proxy = True
def save(self, *args, **kwargs):
self.slug = slugify(self.name)
if not self.id:
self.date_created = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.isactive = 1
self.date_last_modified = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
super(TblGroup, self).save(*args, **kwargs)
models.py
class TblGroup(models.Model):
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=250, blank=True)
date_created = models.DateTimeField(blank=True, null=True)
date_last_modified = models.DateTimeField(blank=True, null=True)
creator = models.ForeignKey(AuthUser, blank=True, null=True)
group_photo_url = models.CharField(max_length=250, blank=True)
isactive = models.IntegerField(blank=True, null=True)
slug = models.CharField(max_length=250, blank=True)
code = models.IntegerField(blank=True, null=True)
class Meta:
managed = False
db_table = 'tbl_group'
Summary
Basically, what I need is to automatically update date_last_modified, date_created and slug when I save them, and do NOT update any other part to NULL.
Obviously, my erroneous part is this in view:
group = eiysTblGroup(id = group_id )
I'm not sure how I made such a silly mistake.
The correct form should be:
group = eiysTblGroup.objects.get(id = group_id )
Then it works correctly...
I believe the answer for your question can be found here:
Automatic creation date for django model form objects?
You want to use those, because Django can set creation and modification dates for you automatically without any additional interactions needed.
models.DateTimeField(auto_now_add=True)
models.DateTimeField(auto_now=True)
As for the slug, shorten your save() method to:
def save(self, *args, **kwargs):
self.slug = slugify(self.name)
super(eiysTblGroup, self).save(*args, **kwargs)

Categories