I have three models, Person, Role and Student. Person is related to role with ManyToManyField and Student inherits from Person. The code:
class Person(models.Model):
#fields
class Role(models.Model):
ROLE_CHOICES = (
('AL', 'ALUMNO'),
#... more choices
)
person = models.ManyToManyField(Person)
role = models.CharField(
'Rol',
max_length = 2,
choices = ROLE_CHOICES
)
class Student(Person):
# fields
def save(self, *args, **kwargs):
super(Student, self).save(*args, **kwargs)
# what code should I put here to save role?
I want to, on saving a Student, automatically save a role for him/her ('AL'). Also it has to execute on create and not in update.
I've seen other posts addresing this, but it remains unclear to me how
to implement this.
As I understand, I can override the save method, but I'm not sure how exactly do this. I'm aware that post_save signal can also accomplish this, but I'm not sure how either.
Thanks.
You can't do this before student get it's pk by save method,because m2m relations is establish by your instance pk and pk is generate after your instance save to db.
two way to archieve:
First one:
new a signals.py file in your app:
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import *
#receiver(post_save, sender=Student)
def create_student(sender, instance=None, created=False, **kwargs):
if created:
role, is_created = Role.objects.get_or_create(name='AL')
role.person.add(instance)
and in your apps.py
class UserConfig(AppConfig):
name = 'user'
def ready(self):
import user.signals
replace User with your own app_label.
Second one:
after django 1.9,django has transaction tool allow you performing actions after commit.doc is here:
from django.db import transaction
class Student(Person):
.
.
.
def save(self, *args, **kwargs):
instance = super(Student, self).save(*args, **kwargs)
if not self.pk:
# do when create
transaction.on_commit(self.update_role)
return instance
def update_role(self):
# this will be call after instance save to db
role, is_created = Role.objects.get_or_create(name='AL')
role.person.add(self)
all code is untested.
Related
I'm following the 'User profile' approach to extend my User model, like so:
# models.py
class UserProfile(models.Model):
user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE, primary_key=True)
my_field = models.CharField(max_length=100)
# signals.py
#receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
With this approach, I have to explicitly call user.profile.save(), which to me feels clunky, as I want the profile to give the illusion it is part of the User object:
# views.py
def some_func(request):
user = User.objects.create_user('dummy', 'dummy#dummy.com', '12345678')
user.profile.my_field = 'hello'
user.save() # This does not persist the profile object...
user.profile.save() # ...this does
To remedy this, I've changed create_user_profile() to the following, which works:
# signals.py
#receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
profile = UserProfile.objects.get_or_create(user=instance)
profile.save()
Numerous examples I've encountered do not use this approach. Are there any caveats to using this approach?
The better way is to specify a custom user model.
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
custom_field = models.ForeignKey(
'contracts.Contract'
)
...
class Meta(AbstractUser.Meta):
swappable = 'AUTH_USER_MODEL'
You have to update the settings.py defining the AUTH_USER_MODEL property:
AUTH_USER_MODEL = 'app_name.User'
You can use a custom User model like this :
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
def __str__(self):
return f'{self.user.username} Profile'
and then the signals.py file :
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
from .models import Profile
#receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
#receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
instance.profile.save()
what the signals.py file does here is to inform you when a new Profile object is created of the User type which you can further use to create forms that update/create the user's profile.
And to make all this work, you need to import the signals.py file in the apps.py file of your app. For example, this is what your apps.py file would look like :
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'
def ready(self):
import users.signals
Yes, there are a few. In the following situations the post_save signal would not be fired.
1 If the save method does not successfully save the object (such as when an IntegrityError occurs)
2 When you call MyModel.objects.update()
3 When you override the save method and forget to call the superclass method.
4 When your signal receiver hasn't been successfully registered.
In these situations your profile wouldn't be saved.
I have a Django model which includes specific permissions. I want to be able to assign permissions to those users in the assigned_to field. I am using django-guardian to manage the permissions.
from django.db import models
from django.contrib.auth.models import User
from guardian.shortcuts import assign_perm
class Project(models.Model):
...
assigned_to = models.ManyToManyField(
User, default=None, blank=True, null=True
)
created_by = models.ForeignKey(
User,
related_name="%(app_label)s_%(class)s_related"
)
permissions = (
('view_project', 'View project'),
)
I have tried to implement a custom save method. If I try and do it before the save:
def save(self, *args, **kwargs):
for user in self.assigned_to:
assign_perm('view_project', user, self)
super(Project, self).save(*args, **kwargs)
I get an error:
ObjectNotPersisted: Object <project_name> needs to be persisted first
If I do it after the save (which I guess is wrong anyhow):
def save(self, *args, **kwargs):
super(Project, self).save(*args, **kwargs)
for user in self.assigned_to:
assign_perm('view_project', user, self)
I get an Type Error 'ManyRelatedManager' object is not iterable.
Should I be using a post-save signal for this? What is the best approach for what I assume is a common pattern?
The error is caused because the field itself is not iterable, you need to specify the queryset using filter or all:
for user in self.assigned_to.all():
assign_perm('view_project', user, self)
However, as you commented, this needs to be done after the parent model instance is saved, so yes you can either create a post_save signal to accomplish this or save the M2M relations in your view after your call to save the parent instance.
let's say I've the following very simple models:
class Customer(models.Model):
name = models.CharField(max_length=50)
class Probe(models.Model):
OwnerInfo = models.CharField(max_length=50)
comments = models.CharField(max_length=50)
customer = models.ForeignKey(Customer, null=True, blank=True)
I've been able to add an InLine to the Admin gui, but I'd like to use a SELECT component, so I can just select several Probes and assign them to the Customer. From this question:
one-to-many inline select with django admin
I know thanks to Luke's answer (last one) that I should create a custom Form and assign it to my ModelAdmin.form but I can not wonder how to tie it all together to make it work.
May anyone help?
Thanks a lot in advance.
OK, I came a step further, and now I've the field added to the Form, like this:
from django.contrib import admin
from django import forms
from web_gui.models import Probe, Customer, Firmware
class CustomerForm(forms.ModelForm):
probes = forms.ModelMultipleChoiceField(queryset=Probe.objects.all())
def __init__(self, *args, **kwargs):
super(CustomerForm, self).__init__(*args, **kwargs)
self.fields['probes'].initial = [p.pk for p in Probe.objects.filter(customer_id=self.instance.pk)]
class Meta:
model = Customer
class CustomerAdmin(admin.ModelAdmin):
form = CustomerForm
admin.site.register(Probe)
admin.site.register(Customer, CustomerAdmin)
admin.site.register(Firmware)
but the initial values specified through "initial" are not being selected. What's wrong now? I assume that next will be to override the save() method to set the Probes on the Customer, am I right?
This is the best solution I've came up with. Let me know if there is any other better way of achieving this:
from django.contrib import admin
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from web_gui.models import Probe, Customer, Firmware
class CustomerForm(forms.ModelForm):
probes = forms.ModelMultipleChoiceField(queryset = Probe.objects.all(), required=False)
probes.widget = FilteredSelectMultiple("Probes",False,attrs={'rows':'10'})
def __init__(self, *args, **kwargs):
super(CustomerForm, self).__init__(*args, **kwargs)
self.fields['probes'].initial = [p.pk for p in Probe.objects.filter(customer_id=self.instance.pk)]
def save(self, force_insert=False, force_update=False, commit=True):
c = super(CustomerForm, self).save(commit=False)
c.probe_set = self.cleaned_data['probes']
c.save()
return c
class Meta:
model = Customer
class CustomerAdmin(admin.ModelAdmin):
form = CustomerForm
admin.site.register(Probe)
admin.site.register(Customer, CustomerAdmin)
admin.site.register(Firmware)
My application has some hundred users and about 10 to 20 groups. Now the customer wants to give special privileges to group leaders.
How would you do this (in a reusable way)?
You can create a new model that extends the group class and add it a new foreign key to a user (the leader)
#------------------------------------------
# models.py of fooapp
from django.db import models
from django.contrib.auth.models import User, Group
class GroupLeader(models.Model):
user=models.OneToOneField(User, primary_key=True)
groups=models.ManyToManyField(Group, related_name='leaders')
class Meta:
verbose_name_plural=u'Group Leaders'
The next snippet makes the group leaders available in django admin:
#-----------------------------------------
# admin.py of fooapp
# See http://stackoverflow.com/questions/2216974/django-modelform-for-many-to-many-fields
from django.contrib.auth.admin import GroupAdmin
from fooapp.models import GroupLeader
class MyGroupForm(forms.ModelForm):
class Meta:
model=Group
leaders = forms.ModelMultipleChoiceField(queryset=User.objects.all())
def __init__(self, *args, **kwargs):
if 'instance' in kwargs:
# We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist.
initial = kwargs.setdefault('initial', {})
# The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data.
initial['leaders'] = [t.pk for t in kwargs['instance'].leaders.all()]
forms.ModelForm.__init__(self, *args, **kwargs)
self.fields['leaders'].widget.attrs['size']=10
def save(self, commit=True):
instance = forms.ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form,
old_save_m2m = self.save_m2m
def save_m2m():
old_save_m2m()
instance.leaders.clear()
for leader_user in self.cleaned_data['leaders']:
leader_obj, created = GroupLeader.objects.get_or_create(pk=leader_user.pk) # User object to GroupLeader object
instance.leaders.add(leader_obj)
self.save_m2m = save_m2m
# Do we need to save all changes now?
if commit:
instance.save()
self.save_m2m()
return instance
class MyGroupAdmin(GroupAdmin):
form=MyGroupForm
admin.site.unregister(Group)
admin.site.register(Group, MyGroupAdmin)
I have 2 models.py files in different app directories: users.models.py and friends.models.py.
There is one problem: if some user deleted from UserProfile model, all his friendship network must be deleted too with him. It is very natural.
But when I import Frienship to users.model.py I've got an error: Cannot import name Friendship
I understand an error arose because of mutual importing in these 2 files, and I know I can easely solve this problem with the help of signals, but I do not know how to do it in proper way.
Could anybody help in this particular case?
In users.models.py:
from friends.models import Friendship
class UserProfile(models.Model):
username = models.Charfield(max_length=50)
...
def delete(self, *args, **kwargs):
Friendship.objects.remove_all(self)
self.delete(*args, **kwargs)
In friends.models.py:
from users.models import UserProfile
class FriendshipManager(models.Manager):
def remove_all(self, user):
usr = Friendship.objects.get(user=user).friends
frs = [i.user for i in usr.all()]
for fr in frs:
usr.remove(fr)
class Friendship(models.Model):
user = models.Foreignkey(UserProfile)
friends = models.ManyToManyField('self')
objects = FriendshipManager()
Thanks in advance!!!
No need for signals, just use the magic *_set property. It will be defined at runtime so you don't have to worry about circular imports.
friends/models.py
from users.models import UserProfile
class Friendship(models.Model):
user = models.Foreignkey(UserProfile)
friends = models.ManyToManyField('self')
users/models.py
class UserProfile(models.Model):
username = models.Charfield(max_length=50)
...
def delete(self, *args, **kwargs):
for f in self.friendship_set.all():
f.delete()
super(self.__class__, self).delete(*args, **kwargs)
This is what I usually do, you could easily refactor this to implement with your model
from django.db import models
from django.db.models.signals import post_save, post_delete
from django.contrib.auth.models import User
# App specific
class UserProfile(models.Model):
"""
Adds a basic UserProfile for each User on the fly.
"""
user = models.OneToOneField(User)
avatar = models.ImageField(upload_to='example/somewhere', blank=True)
def __str__(self):
return "%s's profile" % self.user
def get_avatar(self):
return '/'+self.avatar
def clean_avatar(self):
# TODO: imagekit
pass
class Meta:
app_label = 'users'
def save(self, *args, **kwargs):
if self.id:
this = UserProfile.objects.get(id=self.id)
if this.avatar:
if this.avatar != self.avatar:
this.avatar.delete(save=False)
super(UserProfile, self).save()
def create_user_profile(sender, instance, created, **kwargs):
if created:
profile, created = UserProfile.objects.get_or_create(user=instance)
post_save.connect(create_user_profile, sender=User)
def UserProfileDelete(instance, **kwargs):
"""
Documentatie
"""
instance.avatar.delete(save=False)
post_delete.connect(UserProfileDelete, sender=UserProfile)