Django - How to dynamically create signals inside model Mixin - python

I'm working on a model Mixin which needs to dynamically set signals based on one attribute.
It's more complicated but for simplicity, let's say the Mixin has this attribute:
models = ['app.model1','app.model2']
This attribute is defined in model which extends this mixin.
How can I register signals dynamically?
I tried to create a classmethod:
#classmethod
def set_signals(cls):
def status_sig(sender, instance, created, *args, **kwargs):
print('SIGNAL')
... do som things
for m in cls.get_target_models():
post_save.connect(status_sig,m)
My idea was to call this method somewhere in class automatically (for example __call__ method) but for now, I just tried to call it and then save the model to see if it works but it didn't.
from django.db.models.signals import post_save
print(post_save.receivers)
Realestate.set_signals()
print(post_save.receivers)
r = Realestate.objects.first()
r.status = 1
r.save()
output
[]
[((139967044372680, 46800232), <weakref at 0x7f4c9d702408; dead>), ((139967044372680, 46793464), <weakref at 0x7f4c9d702408; dead>)]
So you see that it registered those models but no signal has been triggered after saving the realestate.
Do you know how to make it work? Even better without having to call method explicitely?
EDIT:
I can't just put the signals creation inside mixin file because models depends on the string in child model.

If you haven't already solved this:
In the connect method, set weak=False. By default it's True so the locally-defined function reference will get lost if the object instance is garbage collected.
This is likely what's happening to your status_sig function; as you can see in the print out of the post_save receivers, the weakref's are dead so will always just return None
In the Django docs:
weak – Django stores signal handlers as weak references by default. Thus, if your receiver is a local function, it may be garbage collected. To prevent this, pass weak=False when you call the signal’s connect() method.
For more info on weakrefs, see Python docs

Related

Is it possible to register an instance method as a Signal receiver via decorators?

There are multiple ways to register class methods as Signal receivers in Django. Using the built-in receiver decorator is an obvious solution, but can work only with static methods or simple functions:
from django.db.models.signals import post_save
from django.dispatch import receiver
class SignalCollection:
#receiver(post_save)
def some_signal(sender, instance, created, **kwargs):
# code
pass
With instance methods, this decorator won't work as such methods require self as a first parameter. In this case, instantiating after defintion is what works:
class SignalCollection:
def some_signal_a(self, sender, instance, created, **kwargs):
# code
pass
def some_signal_b(self, sender, instance, created, **kwargs):
# code
pass
collection = SignalCollection()
post_save.connect(collection.some_signal_a)
post_save.connect(collection.some_signal_b)
The potential issue with this is that it's not well encapsulated, and in case of many methods contains a lot of repeation.
For solving this issue, I intended to apply custom decorators. My question is: is it possible to create such a decorator, either for the class itself, or for its methods, that can perform connections without instantiating the class?
My research yielded no results, and many of my attempts were failed - adding receiver through method_decorator can work, but in none of my snippets were the signals triggered, meaning the connection did not happen.
Creating a class decorator that decorates all its methods seemed promising:
def signal_collection(target_class):
def _is_acceptable_callable(func):
return (inspect.ismethod(func) or inspect.isfunction(func)) and not inspect.isbuiltin(func)
for name, target_method in inspect.getmembers(target_class):
if not _is_acceptable_callable(target_method):
continue
decorated_method = some_extra_decorator(target_method)
setattr(target_class, name, decorated_method)
return target_class
#signal_collection
class SignalCollection:
def some_signal(self, sender, instance, created, **kwargs):
pass
The advantage of this pattern is that you can easily modify signal_collection to contain wrappers, and then use args and kwargs as inputs similar to the function-based receiver (that is, specifying signal, sender, etc), but still, no such some_extra_decorator could I make that would run without explicitly calling <signal>.connect.
I can't be exactly sure but my theory is that it's not possible at all. A fragment of a sentence suggests that Signals can't be connected arbitrarily on this level.
It has to do something with the fact that Signals should be loaded explicitly in AppConfig.ready, similar to the way Models are registered to Sites.
I'm not confident enough about it, hence the question.
I had a similar problem. method_decorator won't work on signal receivers. They are designed to decorate methods accessed by their instance name. It can be customized to work with signal receivers but I don't think it's worth the effort. I switched to regular functions

Why Is This Method Not Visible from Django Custom Manager?

I have two Django models, one that stores promotion codes and another that tracks who redeemed a particular promotion code. I'm trying to create an instance method that determines whether or not a specific user has redeemed a specific code. The problem is that I'm not seeing one of my PromotionManager methods, 'redeemed_by_user'. Here are my classes:
from django.contrib.auth.models import User
from django.db import models
class PromotionManager(models.Manager):
def redeemed_by_user(self, promotion, user):
redemption_count = PromotionRedeemed.objects.filter(promotion=promotion, redeemer=user).count()
if redemption_count == 1:
return True
elif redemption_count == 0;
return False
else:
raise ValueError('Invalid redemption count')
class Promotion(models.Model):
code = models.CharField(max_length=16)
objects = PromotionManager()
class PromotionRedeemed(models.Model):
promotion = models.ForeignKey('Promotion')
user = models.ManyToManyField(User)
If I start the the Django extension shell_plus and do the following:
In [1]: user = User.objects.get(username='smith')
In [2]: promotion = Promotion.objects.get(code='bigsale')
and then I do this:
In [3]: dir(promotion)
I don't see the redeemed by user method. I was under the impression that I could move methods like this from my class to a custom manager class. Is that not the case? If so, can anyone explain why? As I understand it, class manager methods are supposed to act on table-level queries and class intance methods on row-level objects. Isn't objects.filter acting on the table level? I tried moving the method back to the Promotion class and I can see it there but I'd just like to understand why I can't see it in the manager class.
Whatever you are seeing is absolutely correct but there is small correction that you should make. When you do a dir(some_instance) then you see a property named objects .
objects = PromotionManager()
This line sets all the manager methods to the objects property so if you try to access the method via some_instance.objects.method_name then you will be able to access it although you can't use it because Django doesn't allow this. You will see an error like manager methods are not accessible from instances. dir is supposed to show only those methods which are accessible from your model instance.
From the docs,
A Manager is the interface through which database query operations are provided to Django models. By default, Django adds a Manager with the name "objects" to every Django model class.
A model’s manager is an object through which Django models perform database queries. Each Django model has at least one manager, and you can create custom managers in order to customize database access.
Adding extra manager methods(custom managers) is the preferred way to add “table-level” functionality to your models whereas for “row-level” functionality use model methods.
Objects is a special attribute through which you query your database. It’s an instance of the class django.db.models.Manager; it’s where all the default methods for performing queries against the entire model class — all(), get(), filter(), etc.
The dir() function, with an argument, attempt to return a list of valid attributes for that object.
If you dir(promotion), promotion is an instance of Promotion Model object. It returns the attributes of a Promotion instance, which includes the objects attribute. But, you defined objects as PromotionManager(), and the redeemed_by_user() is a method of the Manager instance.
If you dir(promotion.objects) , django would raise an error, AttributeError: Manager isn't accessible via Poke instances. Because, its true. objects is a Manager available at the class level, not to the instances.
From the docs,
Managers are accessible only via model classes, rather than from model instances, to enforce a separation between “table-level” operations and “record-level” operations.
So, if you dir(Promotion.objects), you could see all custom methods defined in the Manager instance of the model.
You use dir on the wrong object.
Moreover, you replaced default manager with yours.
The first manager applied to a model class has special meaning for Django, and is a default one, so add own manager this way, please:
objects = models.Manager()
<your_custom_name> = PromotionManager()

Delete hooks in ndb PolyModel childclass won't execute

I'm using ndb.polymodel.PolyModel to model different types of social media accounts. After an account has been deleted some cleanup has to be done (depending on the subclass).
I tried implementing a _pre_delete_hook on the subclass, but it never gets executed. When adding the exact same hook to its parent class it does get executed:
class Account(polymodel.PolyModel):
user = ndb.KeyProperty(kind=User)
#classmethod
def _pre_delete_hook(cls, key):
# Works as expected
pass
class TwitterAccount(Account):
twitter_profile = ndb.StringProperty()
#classmethod
def _pre_delete_hook(cls, key):
# Never gets called
pass
t_account = TwitterAccount.get_by_id(1)
t_account.key.delete()
Seems strange to me as I would expect the subclass hook to override its parent. Or is this expected behavior?
Solution:
I missed the fact that a delete operation happens on a Key (not a model instance). The key itself only knows the name of the topmost class (Account in this case).
I ended up defining a custom _delete_hook instance method on the subclass. When _pre_delete_hook gets called, I fetch the entity and then check if its class has a specific delete_hook to execute:
# In subclass:
def _delete_hook(self):
# Do stuff specific for this subclass
return
# In parent class
#classmethod
def _pre_delete_hook(cls, key):
s = key.get()
if hasattr(s, '_delete_hook'):
s._delete_hook()
Unfortunately this is expected though non-obvious behaviour
When you call key delete with a PolyModel you are only calling delete on the parent class.
Have a look at the Key you are calling delete on, you will see the Kind is the parent Model. It then looks up the class via the Kind -> class map which will give you an Account class. Have a read up on how PolyModel works. It stores all Account sub classes as Account and has an extra property that describes the inheritance heirarchy. So that a query on Account will return all Account subclasses, but a query on TwitterAccount will only return that subclass.
Calling ndb.delete_multi won't work either.
If you want a specific PolyModel subclass pre delete hook to run, you will have to add a delete method to the subclass, call that, that can then call the subclass _pre_delete_hook (and may be call super).
But that will cause issues if you ever call key.delete directly

Django - Keeping the original method's work and add new custom validation

This is supposed to be a Django-specific, but I guess it's Python anyway.
Basically, I don't want to override the work of the original method in the class I am inheriting (could be a Model class), but I'd like to add additional validation. Is this possible? Any hint?
class MyUserAdminForm(forms.ModelForm):
class Meta:
model = User
def clean(self):
// do some additional work even though it's cleaned by parent's clean method
Call the super classes clean method:
def clean(self):
super(MyUserAdminForm, self).clean()
# more cleaning
This is a common python thing to do when you subclass something and redefine functionaly but want to make sure you keep the super class functionality. Extremely common when you do an init method, as you always need to ensure the super class constructor gets called to set up the instance.
class ContactForm(forms.Form):
message = forms.CharField()
def clean_message(self):
num_words = len(message.split())
if num_words<4:
raise forms.ValidationError("Too short a message!")
return message
This is the way you add a validation method on a field, and this does ensure that the default cleanup happens. There is no need to call the default cleanup method again.
Source: www.djangobook.com
How it works:
When is_valid() is called on the form object, the system looks for any methods in the class that begin with clean_ and ends with an attribute name. If they do, it runs them after running the default cleanup methods.

When is the "post_save" signal in django activated/called?

I've to update some database tables after saving a particular model. I've used the #receiver(post_save decorator for this. But when in this decorator function, the values are still not saved in the database. I've one to many relation but when I get the current instance that is being saved using kwargs['instance'], it doesn't have child objects. But after saving when I check from shell, it does have child objects. Following is the code that I'm using:
#receiver(post_save, sender=Test)
def do_something(sender, **kwargs):
test = kwargs['instance']
users = User.objects.filter(tags__in=test.tags.values_list('id',flat=True))
for user in users:
other_model = OtherModel(user=user, test=test, is_new=True)
other_model.save()
post_save is sent at the end of Model.save_base(), which is itself called by Model.save((). This means that if you override your model's save() method, post_save is sent when you call on super(YourModel, self).save(*args, **kw).
If Tag has a ForeignKey on Test and the Test instance was just created, you can't expect to have any Tag instance related to your Test instance at this stage, since the Tag instances obviously need to know the Test instance's pk first so they can be saved too.
The post_save for the parent instance is called when the parent instance is saved. If the children are added after that, then they won't exist at the time the parent post_save is called.

Categories