Django: Programmatically add Groups on User save - python

After a user is saved, I need to make sure that its instance is associated with a group by default.
I have found two ways to achieve that:
Overriding the model's save() method
models.py:
from django.contrib.auth.models import AbstractUser, Group
class Person(AbstractUser):
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
to_add = Group.objects.get(id=1) # get_or_create is a better option
instance.groups.add(to_add)
Capturing a post_save signal:
signals.py:
from django.conf import settings
from django.contrib.auth.models import Group
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(
post_save,
sender=settings.AUTH_USER_MODEL,
)
def save_the_group(instance, raw, **kwargs):
if not raw:
to_add = Group.objects.get(id=1) # get_or_create is a better option
instance.groups.add(to_add)
Are these methods equal in achieving their goal?
Is there a better one in Django terms of "Good Practice"?

Update
Acquiring a better understanding of how Django works, I see that the
confusion and also the solution lie in BaseModelForm.save():
...
if commit:
# If committing, save the instance and the m2m data immediately.
self.instance.save()
self._save_m2m()
...
and in BaseModelForm._save_m2m():
...
if f.name in cleaned_data:
f.save_form_data(self.instance, cleaned_data[f.name])
...
The instance is first saved to acquire a primary key (post_save
signal emmited) and then all its many to many relations are saved based
on ModelForm.cleaned_data.
If any m2m relation has been added during the post_save signal or at
the Model.save() method, it will be removed or overridden from
BaseModelForm._save_m2m(), depending on the content of the
ModelForm.cleaned_data.
The transaction.on_commit() -which is discussed as a solution in this
asnwer later on and in a few other SO answers from which I was inspired
and got downvoted- will delay the changes in the signal until
BaseModelForm._save_m2m() has concluded its operations.
Although, in some special cases the transaction.on_commit() is very useful, in this case is an overkill, not only because it is complexing the situation in
an awkward manner (the most suitable signal is m2m_changed as explained here) but because avoiding signals altogether, is rather
good.
Therefore, I will try to give a solution that caters for both occasions:
If the instance is saved from Django Admin (ModelForm)
If the instance is saved without using a ModelForm
models.py
from django.contrib.auth.models import AbstractUser, Group
class Person(AbstractUser):
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if not getattr(self, 'from_modelform', False): # This flag is created in ModelForm
<add - remove groups logic>
forms.py
from django import forms
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.models import Group
from my_app.models import Person
class PersonChangeForm(UserChangeForm):
def clean(self):
cleaned_data = super().clean()
if self.errors:
return
group = cleaned_data['groups']
to_add = Group.objects.filter(id=1)
to_remove = Group.objects.filter(id=2)
cleaned_data['groups'] = group.union(to_add).difference(to_remove)
self.instance.from_modelform = True
return cleaned_data
class Meta:
model = Person
fields = '__all__'
This will either work with:
>>> p = Person()
>>> p.username = 'username'
>>> p.password = 'password'
>>> p.save()
or with:
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model
from django.forms.models import modelform_factory
user_creationform_data = {
'username': 'george',
'password1': '123!trettb',
'password2': '123!trettb',
'email': 'email#yo.gr',
}
user_model_form = modelform_factory(
get_user_model(),
form=UserCreationForm,
)
user_creation_form = user_model_form(data=user_creationform_data)
new_user = user_creation_form.save()
Old answer
Based on either this or that SO questions along with an
article titled "How to add ManytoMany model inside a post_save
signal" the solution I turned to, is to use on_commit(func, using=None):
The function you pass in will be called immediately after a
hypothetical database write made where on_commit() is called would be
successfully committed.
from django.conf import settings
from django.contrib.auth.models import Group
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
def on_transaction_commit(func):
''' Create the decorator '''
def inner(*args, **kwargs):
transaction.on_commit(lambda: func(*args, **kwargs))
return inner
#receiver(
post_save,
sender=settings.AUTH_USER_MODEL,
)
#on_transaction_commit
def group_delegation(instance, raw, **kwargs):
to_add = Group.objects.get(id=1)
instance.groups.add(to_add)
The above code does not take into account that every login causes a
post_save signal.
Digging Deeper
A crucial point made in the relevant Django ticket is that the
above code will not work if a save() call is made inside an
atomic transaction together with a validation that depends on the
result of the group_delegation() function.
#transaction.atomic
def accept_group_invite(request, group_id):
validate_and_add_to_group(request.user, group_id)
# The below line would always fail in your case because the
on_commit
# receiver wouldn't be called until exiting this function.
if request.user.has_perm('group_permission'):
do_something()
...
Django docs describe in more details the constraints under which
on_commit() successfully works.
Testing
During testing, it is crucial to use the
TransactionTestCase or the
#pytest.mark.django_db(transaction=True) decorator when testing with pytest.
This is an example of how I tested this signal.

Related

How to Update value of a Model with another Model's value in Django

class Trade(models.Model):
pips = models.FloatField(default=0)
direction = models.CharField(max_length=30)
new_balance = FloatField(default=0.0)
...
class Summary(models.Model):
winning_trades = models.IntegerField(default=0)
account_balance = FloatField(default=0.0)
...
When a user post a request he/she will populate the Trade model, this will update the summary model and send back to the user new summary data. How can I do this in an elegant way?
You're most likely looking for Django Signals. You'd want your Trade model's create event to trigger a post_save signal that a listener will receive and process.
Assuming you have saved your models in a file models.py,
Create a file signals.py with the following:
# code
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Trade, Summary
#receiver(post_save, sender=Trade)
def update_summary(sender, instance, created, **kwargs):
if created:
# query to update Summary as needed
You'll have to add the signals to your app config.
in your apps.py of the relevant app, add the following:
from django.apps import AppConfig
class AppnameConfig(AppConfig):
name = 'appname'
**def ready(self):
import appname.signals**
First, I will encourage you to create a transaction to perform these two actions. If the second one fails, your database will remain consistent.
Then, Django allows you to override the model methods such as save. You should try that with something like the following:
class Trade():
...
def save(self, *args, **kwargs):
with transaction.atomic():
super.save()
update_summary()
Then, in the view, you could query for the recently updated Summary and return it.
class TradeViewSet():
def create(self, request, *args, **kwargs):
user = request.user
trade = Trade.create(request.data)
updated_summary = get_summary(user.id)
response = SummarySerializer(updated_summary)
return Response(response)

Correct way to write Django post_save signal configuration

I've been trying to learn how to use Signals, and unfortunately, my code below doesn't work. In addition, I've come across a dozen posts that all show different ways to do this.
My goal with this post is to clarify and come to a consensus on writing signals in the best way possible in 2019 for scalability. In this example using post_save.
model.py
from django.db import models
from metrics.models import InventoryProduct
from product.models.product import Product
class PurchasedOrder(models.Model):
__quantity_sold = None
product = models.ForeignKey(Product, on_delete=models.CASCADE)
purchase_quantity = models.IntegerField()
quantity_sold = models.IntegerField(default=0)
def __init__(self, *args, **kwargs):
super(PurchasedOrder, self).__init__(*args, **kwargs)
self.__original_quantity_sold = self.quantity_sold
def save(self, *args, **kwargs):
# If the quantity sold field has changed run the update_sold_out method
if self.quantity_sold != self.__original_quantity_sold:
PurchasedOrderLogic.update_sold_out(self)
super(PurchasedOrder, self).save(*args, **kwargs)
self.__original_quantity_sold = self.quantity_sold
def __str__(self):
return self.product.name
signals.py:
from django.db.models.signals import post_save
from django.dispatch import receiver
from purchasing.models import PurchasedOrder
from purchasing.services.models_logic import PurchasedOrderLogic
#receiver(post_save, sender=PurchasedOrder)
def update_inventory_product_total_qty_purchased(sender, instance):
PurchasedOrderLogic.update_inventory_product_total_qty_purchased(
sender, instance.product.id, instance.purchase_quantity
)
This is where my logic resides. When I update the PurchasedOrder purchase_quantity field, it should update also the InventoryProduct's total_qty_purchased field.
purchasing.services.models_logic.py
class PurchasedOrderLogic(object):
#static_method
def update_inventory_product_total_qty_purchased(purchased_order_class, product_id):
inventory_product = InventoryProduct.objects.get(pk=product_id)
total_qty_purchased = purchased_order_class.objects.filter(product_id=product_id).aggregate(
Sum('purchase_quantity')
).get('purchase_quantity__sum')
inventory_product.total_qty_purchased = total_qty_purchased
inventory_product.save()
apps.py
from django.apps import AppConfig
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _
from purchasing.models import PurchasedOrder
from purchasing.signals import update_inventory_product_total_qty_purchased
class PurchasingConfig(AppConfig):
name = 'purchasing'
verbose_name = _('purchasing')
def ready(self):
post_save.connect(update_inventory_product_total_qty_purchased, sender=PurchasedOrder)
What am missing? I'm not sure even if the signal is running?
I'm triggering .save() by manually modifying any field on the model.
Personally, I've been changing the purchase_quantity field which should print this value as it's called inside the update_inventory_product_total_qty_purchased method

How to link two tables in django?

I use the built-in User model in django, but i want use some custom fields, i was create new model
class Profile(models.Model):
user = models.OneToOneField(User)
profile_img = models.ImageField(null=True)
def __str__(self):
return self.user.username
this is my custom model.
But when i create new user it doesn't display in admin(in profile table), i need to add user from admin panel, after adding it works fine. what i should to do to display Profile info of all users?
p.s. When i was create table profile and was try to select info from joined tabels, sql query wasn't return anything, but after adding existed user to Profile table sql query return all what i want
To create a new Profile object when a new user is created, you can use a pre_save receiver:
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(
user=instance
)
The created parameter is to check if the object has been created ( created=True ) or updated ( created=False )
I am not sure to understand what you want... If you want to manage profile in admin app you have to register it.
from django.contrib import admin
from myproject.myapp.models import Profile
class ProfileAdmin(admin.ModelAdmin):
pass
admin.site.register(Profile, ProfileAdmin)
Edit: you can use a signal to automatically create a profile when creating a user.
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from myproject.myapp.models import Profile
#receiver(post_save, sender=User)
def create_profile(sender, instance **kwargs):
Profile.objects.create(user=instance)
Note that sometime using signals may be hard to maintain. You can also use AbstractBaseUser. This is a classical issue, which is widely covered in a lot of posts.
One I particularly like: https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html

Django - signals. Simple examples to start

I am new to Django and I'm not able to understand how to work with Django signals. Can anyone please explain "Django signals" with simple examples?
Thanks in advance.
You can find very good content about django signals over Internet by doing very small research.
Here i will explain you very brief about Django signals.
What are Django signals?
Signals allow certain senders to notify a set of receivers that some action has taken place
Actions :
model's save() method is called.
django.db.models.signals.pre_save | post_save
model's delete() method is called.
django.db.models.signals.pre_delete | post_delete
ManyToManyField on a model is changed.
django.db.models.signals.m2m_changed
Django starts or finishes an HTTP request.
django.core.signals.request_started | request_finished
All signals are django.dispatch.Signal instances.
very basic example :
models.py
from django.db import models
from django.db.models import signals
def create_customer(sender, instance, created, **kwargs):
print "Save is called"
class Customer(models.Model):
name = models.CharField(max_length=16)
description = models.CharField(max_length=32)
signals.post_save.connect(receiver=create_customer, sender=Customer)
Shell
In [1]: obj = Customer(name='foo', description='foo in detail')
In [2]: obj.save()
Save is called
Apart from the explanation given by Prashant, you can also use receiver decorator present in django.dispatch module.
e.g.
from django.db import models
from django.db.models import signals
from django.dispatch import receiver
class Customer(models.Model):
name = models.CharField(max_length=16)
description = models.CharField(max_length=32)
#receiver(signals.pre_save, sender=Customer)
def create_customer(sender, instance, created, **kwargs):
print "customer created"
For more information, refer to this link.
In the signals.post_save.connect(receiver=create_customer, sender=Customer)... sender will always be the model which we are defining... or we can use the User as well in the sender.
Signals are used to perform any action on modification of a model instance. The signals are utilities that help us to connect events with actions. We can develop a function that will run when a signal calls it. In other words, Signals are used to perform some action on modification/creation of a particular entry in Database. For example, One would want to create a profile instance, as soon as a new user instance is created in Database
There are 3 types of signal.
pre_save/post_save: This signal works before/after the method save().
pre_delete/post_delete: This signal works before after delete a model’s instance (method delete()) this signal is thrown.
pre_init/post_init: This signal is thrown before/after instantiating a model (init() method).
One of the example, if we want to create a profile of a user as soon as the user is created using post_save signal.
For code example, I found the Geeks for Geeks, which explains is very simple way, and easy to understand.
https://www.geeksforgeeks.org/how-to-create-and-use-signals-in-django/
You can add signals to your models.py file
here is an example for adding an auto slug, if you use a SlugField:
this is the stuff you need to import
from django.utils.text import slugify
from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save
Add the #receiver to the bottom of your class, included the def
If you add the def __str__(self): under the receiver, you will get an error
class Store(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=False, blank=True, null=True)
def __str__(self):
return self.name
#receiver(pre_save, sender=Store)
def store_pre_save(sender, instance, *args, **kwargs):
if not instance.slug:
instance.slug = slugify(instance.name)
or you can use post_save
class Store(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=False, blank=True, null=True)
def __str__(self):
return self.name
#receiver(post_save, sender=Store)
def store_post_save(sender, instance, created, *args, **kwargs):
if not instance.slug:
instance.slug = slugify(instance.name)
instance.save()
I found this example from this tutorial

Adding user to group on creation in Django

I'm looking to add a User to a group only if a field of this User is specified as 'True' once the User is created. Every User that is created would have a 'UserProfile' associated with it. Would this be the correct way to implement such a thing?
models.py:
def add_group(sender, instance, created, **kwargs):
if created:
sender = UserProfile
if sender.is_in_group:
from django.contrib.auth.models import Group
g = Group.objects.get(name='Some Group')
g.user_set.add(sender)
post_save.connect(add_group, sender=UserProfile)
Thanks in advance!
Another option is using a post_save signal
from django.db.models.signals import post_save
from django.contrib.auth.models import User, Group
def add_user_to_public_group(sender, instance, created, **kwargs):
"""Post-create user signal that adds the user to everyone group."""
try:
if created:
instance.groups.add(Group.objects.get(pk=settings.PUBLIC_GROUP_ID))
except Group.DoesNotExist:
pass
post_save.connect(add_user_to_public_group, sender=User)
Only trouble you will have is if you use a fixture ... (hence the DoesNotExists .. )
try this:
def save(self, *args, **kwargs):
# `save` method of your `User` model
# if user hasnt ID - is creationg operation
created = self.id is None
super(YourModel, self).save(*args, **kwargs)
# after save user has ID
# add user to group only after creating
if created:
# adding to group here

Categories

Resources