Django: Help understanding a signal-function - python

Good evening,
I know, I asked a similar Question just a while ago, but I still got problems in understanding the more detailled functions of a signal. So I'm hoping someone can help me out with this one!? As an example I have a class in my "models.py" like this one:
class ExampleModel(models.Model):
name = models.CharField(max_length=255, null=False, blank=False)
value1 = models.IntegerField()
value2 = models.IntegerField()
value_of_1_and_2 = models.IntegerField()
def __str__(self):
return self.name
I would like to have my field "value_of_1_and_2" filled automatically with the sum of the other two fields before saving...
Is this possible? I tried some things and got stuck with a "pre_save" like this:
#receiver(pre_save, sender=ExameplModel)
def save(self, *args, **kwargs):
...
Thanks for all of your help and a nice evening to all of you!

Although the signals do logically similar things, they are slightly different from the save method in the model class. To be more specific, they answer the following questions in your head:
What do I want to do before the model is saved/deleted?
What do I want to do after the model is saved/deleted?
Others ..
If we come to what you want; If I understood correctly, you want to leave value_of_1_and_2 field blank while saving your model, and you want django to save it in the background with a method you set.
First of all, I prefer to keep the signals and models apart for a clean look. So next to the models.py file, create the signals.py file.
signals.py :
from .models import ExampleModel
from django.db.models.signals import pre_save
from django.dispatch import receiver
#receiver(pre_save, sender=ExampleModel)
def pre_save_example_model(sender, instance, *args, **kwargs):
if not instance.value_of_1_and_2:
instance.value_of_1_and_2 = instance.value1 + instance.value2
Then make the following definition in apps.py for the signals to work (Let's consider the name of the application as Summing. You can replace it with your own.):
apps.py :
from django.apps import AppConfig
class SummingConfig(AppConfig):
name = 'summing'
def ready(self):
from . import signals
And the most important thing here is: since you will leave the value_of_1_and_2 field empty, you should update your field as follows. Otherwise Django will raise the error.
value_of_1_and_2 = models.IntegerField(blank=True, default=0)
That's all. Save the model by entering value1 and value2, then you will see the result.

Related

ImportError: cannot import name 'Flow' from partially initialized module 'firstapp.models.flows' (most likely due to a circular import), how to design

I have a problem, probably in my design.
In my Django project I have a 'Flow' object with 'event_reason' field in it, Foreign key to 'Event' object, therefore I need to import Event object, (otherwise I get the error: Cannot create form field for 'event_reason' yet, because its related model 'Event' has not been loaded yet). Therefore my code of my models is (in models/flows.py)
from .events import Event
class Flow(models.Model):
amount = models.DecimalField(max_digits=17, decimal_places=2, default=0.0)
flow_type = models.IntegerField(default=-1, choices=FLOW_CHOICES)
...
event_reason = models.ForeignKey('Event', on_delete=models.DO_NOTHING, default=None, related_name='events_flow',blank=True, null=True)
class FlowForm(ModelForm):
class Meta:
model = Flow
fields = '__all__'
In addition I have 'amount' and 'flow_type' fields in the 'Flow' objects and I want that the 'post_save' signal will save a new Flow associated to this Event. Therefore the Event model code is (in models/events.py):
from .flows import Flow
class Event(models.Model):
...
amount = models.IntegerField(default=0)
flow_type = models.IntegerField(default=-1, choices=FLOW_CHOICES)
#receiver(post_save, sender=Event)
def my_handler(sender, **kwargs):
new_flow = Flow.objects.create(...)
And I get the error ImportError: cannot import name 'Flow' from partially initialized module 'firstapp.models.flows' (most likely due to a circular import)
I understand that I cannot my a circular import but I want this functionality to work. How to solve this problem? Should I design the objects otherwise?
Thank you
The models.py should mostly only have model code, sometimes people do put signals (bad practice) but forms should absolutely not be written there.
One should write forms in forms.py so your should move that code to a separate file and import the model there. The forms etc. should not be imported in the models as their should be no need for them there.
So in forms.py:
from .models.flows import Flow
class FlowForm(ModelForm):
class Meta:
model = Flow
fields = '__all__'
# Other forms also go here
Now for signals it is best not to write them in models as I said before. They should be written in some file let's say signals.py and this will be imported in the apps config classes ready method.
So in signals.py:
# imports
#receiver(post_save, sender=Event)
def my_handler(sender, **kwargs):
new_flow = Flow.objects.create(...)
Now your app should have an app config by default in which you would add an import in the ready method. So in your apps apps.py just add an import in the ready method:
class YouAppConfig(AppConfig):
# some attributes here don't change
def ready(self):
from . import signals
Now remove those circular imports in your model files.

Django elasticsearch-dsl updating M2M on pre_save

I am using the django-elasticsearch-dsl package and am having a little bit of a dilemma. Here is my code:
models.py
class Source(models.model):
name = models.CharField(max_length=50)
class Posting(models.Model):
title = models.CharField(max_length=250)
sources = models.ManyToMany(Sources, related_name="postings", through="PostingSource")
class PostingSource(models.Model):
posting = models.ForeignKey(Posting, related_name="posting_sources", on_delete=models.CASCADE)
source = models.ForeignKey(Source, related_name="posting_sources", on_delete=models.CASCADE)
documents.py
class PostingDocument(Document):
sources = fields.ObjectField(properties={"name": fields.KeywordField()})
class Index:
name = "posting"
settings = {"all the settings stuff"}
class Django:
model = Posting
fields = ["title"]
related_models = [PostingSource]
def get_queryset(self):
return super().get_queryset().select_related("sources")
def get_instance_from_related(self, related_instance):
if isinstance(related_instance, PostingSource):
return related_instance.posting
My issue is, when I update the sources on the posting, for some reason, the elasticsearch index is updated pre_save and not post_save. I basically have to do a put request 2 times with the same sources in order for the changes to reflect in my index. I added a def prepare_sources(self, instance): as part of my document and it seems to work, but it feels like it will cause performance issues later. Any help or guidance will be greatly appreciated.
After a couple months of testing, I solved my first question by adding a def prepare_sources(self, instance): where I denormalize my many to many relationship. I also sort of solved my second question by simply having the .select_related("sources") which helps with performance.

Custom django signal not updating my models

I am trying to implement a custom signal but I have never done this before, so I seem to have messed this up.
Here is my code from models.py:
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save, post_delete
from django.dispatch import Signal
from django.db.models import Max,Count
from django.apps import apps
# Create your models here.
class Player(models.Model):
player_name = models.CharField(max_length=150)
current_level_no = models.IntegerField(null=True)
no_of_moves = models.IntegerField(null=True)
class Meta:
verbose_name_plural = 'Players'
def __str__(self):
return self.player_name
class PlayerStats(models.Model):
player_name = models.ForeignKey(to=Player, on_delete=models.CASCADE)
level_no = models.IntegerField(null=True)
moves = models.IntegerField(null=True)
class Meta:
verbose_name_plural = 'Players Stats'
# It works!
TotalLevels = 25
MaxCurrentLevel = PlayerStats.objects.aggregate(max_levels=Max('level_no'))['max_levels']
PlayerCount = Player.objects.aggregate(count_players=Count('player_name', distinct=True))['count_players']
def create_player(sender, instance, created, **kwargs):
if created:
new_username=instance.username
Player.objects.create(player_name=new_username, current_level_no=None, no_of_moves=None)
def delete_player(sender, instance, **kwargs):
deleted_username=instance.username
Player.objects.filter(player_name=deleted_username).delete()
def create_player_stat(sender, instance, **kwargs):
for x in range(1, TotalLevels+1):
PlayerStats.objects.create(player_name=instance, level_no=x, moves=None)
UpdateLevelsSignal = Signal(providing_args=['Update'])
if MaxCurrentLevel != TotalLevels and PlayerCount != 0:
UpdateLevelsSignal.send(UpdateLevelsSignal,Update=True)
def UpdateLevels(sender, Update,**kwargs):
if Update:
if MaxCurrentLevel < TotalLevels:
for x in Player.objects.all().values_list('player_name', flat=True):
instance = Player.objects.get(player_name=x)
for y in range(TotalLevels-MaxCurrentLevel, TotalLevels+1):
PlayerStats.objects.create(player_name=instance, level_no=y, moves=None)
else:
for x in Player.objects.all().values_list('player_name', flat=True):
instance = Player.objects.get(player_name=x)
for y in range(MaxCurrentLevel-TotalLevels, MaxCurrentLevel+1):
PlayerStats.objects.filter(player_name=instance, level_no=y, moves=None).delete()
post_save.connect(create_player, sender=User)
post_delete.connect(delete_player, sender=User)
post_save.connect(create_player_stat, sender=Player)
UpdateLevelsSignal.connect(UpdateLevels, sender=UpdateLevelsSignal)
Basically a signal to create or delete some model objects based on certain conditions, nothing too fancy. But, when I check on the model objects, there are no changes occurring on triggering the conditions.
If someone could point out what I am doing wrong here or suggest a solution, that would be helpful!
As always, I greatly appreciate your answers!
This isn't going to solve your question, but I don't want to include it on a comment. The reason you should move your signals and receivers into their own files is limit how much business logic is in your models file. Plus it makes it more extensible when dealing with multiple apps.
project/
app1/
apps.py
models.py
signals.py
receivers.py
app2/
apps.py
models.py
receivers.py
In each of the apps above, the apps.py file would be something like:
class App1Config(AppConfig):
def ready(self):
# This makes Django load up the register the connected receivers.
from project.app1 import receivers, signals
The reason you want to move the receivers into their own files is partly due to the case of wanting to trigger a function that only impacts models/objects in app2, but is triggered by some event on a model in app1. For example:
#project/app2/receivers.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from project.app1.models import Foo
from .models import Bar
#receiver(post_save, sender=Foo)
def create_bar_for_foo(instance, created, *, raw=False, **kwargs):
if created:
Bar.objects.create(foo=foo)
You could do that receiver in app2/models.py, but it would feel out of place. It would feel especially alien if it were a custom signal that has nothing to do with model-based events.
I wish the Django Documentation attempted to explain how to organize the signals and receivers, but the book Two Scoops of Django does a great job of explaining it.

Django: Update Model-Field everytime a Foreign-Key is associated with said model?

Lets suppose we have the following classes:
class Cart(models.Model):
total = models.DecimalField(default=0.00, max_digits=100, decimal_places=2)
and
class CartItem(models.Model):
cart = models.ForeignKey(Cart, related_name='items', on_delete=models.CASCADE)
I'd like to update the total everytime a Cart-Item is added to the Cart. When/ How can I call my addItUp() method here?
Thought about using a signal like this, but this doesn't trigger:
#receiver(post_save, sender=CartItem, weak=False)
def post_save_cartItem_receiver(sender, instance, *args, **kwargs):
print("inside signal")
Here's my very simplified addToCart() method:
def add(self, request, product_id, cart_obj):
product = get_object_or_404(Product, id=product_id)
CartItem.objects.create(cart=cart_obj, ...)
cart_obj.save()
return True
This is my first question here, if i can better my explanation please tell me.
Welcome to StackOverflow.
A common problem that people have when creating triggers is that they forget (or don't know) to import signals in their apps.py file. That's the only reason that I can imagine your signal wouldn't be triggering.
I imagine your apps.py file should look something like (if the name of your app is Shop):
from django.apps import AppConfig
class ShopConfig(AppConfig):
name = 'Shop'
def ready(self):
import Shop.signals
You can then use the post_save signal for a CartItem to get its related Cart, add up the total value, and save the Cart with a new total.

How to add custom function to admin forms?

I would like to implement a function that updates quantity in LibraryBook each time the admin adds a book in SingleBook on the admin site. I have been searching for means to do so but to no avail. Any pointers including links to documentation would be very much appreciated.
Here is my code:
#models.py
class LibraryBook(models.Model):
book_title = models.CharField(max_length=100, blank=False)
book_author_id = models.ForeignKey(BookAuthors, on_delete=models.CASCADE)
category = models.ForeignKey(BookCategory, on_delete=models.CASCADE)
quantity = models.IntegerField(blank=False, default=0)
number_borrowed = models.IntegerField(default=0)
def __unicode__(self):
return unicode(self.book_title)
class SingleBook(models.Model):
serial_number = models.CharField(primary_key=True , max_length=150, blank=False)
book_id = models.ForeignKey(LibraryBook, on_delete=models.CASCADE)
is_available_returned = models.BooleanField(default=True)
is_borrowed = models.BooleanField(default=False)
def __unicode__(self):
return unicode(self.book_id)
#admin.py
class SingleBookAdmin(admin.ModelAdmin):
list_display = ('book_id', 'serial_number')
class LibraryBookAdmin(admin.ModelAdmin):
list_display = ('book_title', 'book_author_id', 'quantity')
search_fields = ('book_title', 'book_author_id')
fields = ('book_title', 'book_author_id', 'quantity')
PS: I have omitted the import and admin.site.register code
Django==1.9.8
django-material==0.8.0
django-model-utils==2.5.1
psycopg2==2.6.2
wheel==0.24.0
override save_model
If you only want to make the changes when an admin updates a record, the best way is to override the save_model method in ModelAdmin
The save_model method is given the HttpRequest, a model instance, a
ModelForm instance and a boolean value based on whether it is adding
or changing the object. Here you can do any pre- or post-save
operations.
class SingleBookAdmin(admin.ModelAdmin):
list_display = ('book_id', 'serial_number')
def save_model(self, request, obj, form, change):
admin.ModelAdmin.save_model(self, request, obj, form, change)
if obj.is_borrowed:
do something to obj.book_id.quantity
else:
do something to obj.book_id.quantity
post_save signal
from django.dispatch.dispatcher import receiver
from django.db.models.signals import post_save
#receiver(post_save, sender=SingleBook)
def user_updated(sender,instance, **kwargs):
''' Fired when a SingleBook is updated or saved
we will use the opporunity to change quantity'''
# your logic here
Other pointers
If on the other hand, you wanted to make changes based on all user actions, catching the post_save signal is the way to go. In either case, you might want to override the from_db method in the model to keep track of which fields have changed.
You might also want to change quantity and number_borrowed to IntegerFields (unless you are only using sqlite in which case it doesn't matter)
Also book_author_id should probably be book_author and book_id should probably be book (this is not a rule, just a convention to avoid the ugly book_id_id reference)
Use signals. Just attach post_save signal to SingleBook model and update according LibraryBook in it. post_save signal takes created argument, so you can determine if book is newly created or edited and apply your action based on that.
Also attach post_delete signal to decrease counter when SingleBook is removed.
To avoid race conditions (when 2 admins are adding books at the same time), I'm suggesting use of queryset update method together with F on changing LibraryBook counter, example:
LibraryBook.objects.filter(id=single_book.book_id_id).update(quantity=F('quantity') + 1)
Doing it that way will ensure that actual math operation will be performed on database level.

Categories