How delete a model instance and the instance from the OneToOne relation - django

I have these two models in django rest:
class CustomUser(AbstractBaseUser):
email = models.EmailField(max_length=255, unique=True)
class Teacher(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
How can I delete the CustomUser instance related when deleting a Teacher instance?
I tried this code but it gives this error "maximum recursion depth exceeded while calling a Python object"
#receiver(pre_delete, sender=Teacher)
def delete_user(sender, instance, **kwargs):
instance.user.delete()

The issue is that you're using the pre_delete signal, and as such, it deletes the CustomUser first. The CustomUser's deletion cascades and deletes your Teacher, but before deleting your Teacher the signal is called again, causing an infinite cycle.
Change pre_delete to post_delete on your #receiver.

In Your settings.py, write this code line after INSTALLED_APPS.
AUTH_USER_MODEL = 'your_app_name.CustomUser'
Models.py:
class Teacher(models.Model):
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE,blank=True, null=True)
View.py or If you have separate Signals.py:
#receiver(pre_delete, sender=CustomUser)
def delete_user(sender, instance, **kwargs):
user = Teacher.objects.get(user=instance)
user.delete()
Try This Code.

Related

Django: 'User' object has no attribute 'perfil_de_usuario'

I'm trying to make a page that, after the user is logued in, shows a list of diferent actions.
The proble is that, when the user is succesfully authenticated, the resulting page is this error:
AttributeError at /iniciar_sesion/
'User' object has no attribute 'perfil_de_usuario'
Request Method: POST
Request URL: http://127.0.0.1:8000/iniciar_sesion/
Django Version: 3.0.3
Exception Type: AttributeError
Exception Value:
'User' object has no attribute 'perfil_de_usuario'
Exception Location: /home/jenifer/Documentos/qbit/mysite4/usuarios/models.py in guardar_usuario_perfil, line 25
The model is as follows:
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
class Perfil_de_Usuario(models.Model):
idusuario = models.AutoField(db_column='idUsuario', primary_key=True)
nombres = models.CharField(max_length=45, blank=True, null=True)
apellidos = models.CharField(max_length=45, blank=True, null=True)
clave = models.CharField(max_length=45, blank=True, null=True)
email = models.CharField(max_length=45, blank=True, null=True)
web = models.URLField(blank=True)
class Meta:
managed = False
db_table = 'Usuario'
#receiver(post_save, sender=User)
def crear_usuario_perfil(sender, instance, created, **kwargs):
if created:
perfil_de_usuario.objects.create(usuario=instance)
#receiver(post_save, sender=User)
def guardar_usuario_perfil(sender, instance, **kwargs):
instance.perfil_de_usuario.save()
For what the error says, the problem is with guardar_usuario_perfil, but I'm not getting how to modify it for this thing to work.
I know there are similar posts and I've tried different solutions like rename instance.perfil_de_usuario.save() part but the result is the same.
If somebody can help me I will apreciate it very much
You should use a OneToOneField so that Django can add a reverse relation on the User model
class Perfil_de_Usuario(models.Model):
idusuario = models.OneToOne(settings.AUTH_USER_MODEL, related_name="perfil_de_usuario")
....
The related_name="perfil_de_usuario" is what Django will add to the User model so that you can do what you were trying to do on the signals.
Also, note I didn't use the user model directly in the OneToOneField but used settings.AUTH_USER_MODEL instead as it is the recommended way.
Here is the Django documentation on extending the user model

Create a separate user type

I am working on an intranet web application which needs two types of users. Normal users that can be setup from django admin and specific type of users -
Employees.
I have the following model for Employee type user.
class Employee(models.Model):
emp_name = models.CharField(max_length=500)
slug = models.SlugField(unique=True, default='')
location = models.CharField(max_length=200)
email = models.EmailField()
experience = models.TextField(blank=True)
primary_skill = models.ManyToManyField(PrimarySkill)
secondary_skill = models.ManyToManyField(SecondarySkill)
I tried having a OneToOneField like this as per the official doc and
this article:
user = models.OneToOneField(User, blank=True, null=True, on_delete=models.CASCADE)
#receiver(post_save, sender=User)
def create_employee(sender, instance, created, **kwargs):
if created:
Employee.objects.create(user=instance)
#receiver(post_save, sender=User)
def save_employee(sender, instance, **kwargs):
instance.employee.save()
I realized that this is the opposite of what I want. Every time a User
is created from the admin, there was an entry created in the
app_employee table.
What I want is this:
Every time an Employee is created, I need a User created.
An Employee can be created using a separate signup form, say emp_signup
How do I approach this scenario?
I have achieved this using a custom user based on AbstractUser inspired by this article.
class CustomUser(AbstractUser):
pass
class Employee(CustomUser):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
# other fields
In settings.py, I then add the following key:
AUTH_USER_MODEL = 'myapp.CustomUser'
And wherever I need to refer the User class, I use get_user_model(), which will substitute our custom user, in views and forms as follows:
views.py
from django.contrib.auth import get_user_model
def user_profile(request):
User = get_user_model()
user = get_object_or_404(User, username=request.user.username)
return render(request, 'user/user_profile.html', {
'site_user': user
})
forms.py
class SignUpForm(UserCreationForm):
class Meta:
model = get_user_model()
fields = ('username', 'email', 'password1', 'password2',)

How can make the admin for a ForeignKey('self') ban referring to itself?

I have a model with a forgein key to itself. For example:
class Folder(models.Model):
name = models.CharField()
parent_folder = models.ForeignKey('self', null=True, blank=True, default=None, on_delete=models.CASCADE)
For my purposes, I never want parent_folder to refer to itself, but the default admin interface for this model does allow the user to choose its own instance. How can I stop that from happening?
Edit: If you're trying to do a hierarchical tree layout, like I was, another thing you need to watch out for is circular parent relationships. (For example, A's parent is B, B's parent is C, and C's parent is A.) Avoiding that is not part of this question, but I thought I would mention it as a tip.
I would personally do it at the model level, so if you reuse the model in another form, you would get an error as well:
class Folder(models.Model):
name = models.CharField()
parent_folder = models.ForeignKey('self', null=True, blank=True, default=None, on_delete=models.CASCADE)
def clean(self):
if self.parent_folder == self:
raise ValidationError("A folder can't be its own parent")
If you use this model in a form, use a queryset so the model itself doesn't appear:
class FolderForm(forms.ModelForm):
class Meta:
model = Folder
fields = ('name','parent_folder')
def __init__(self, *args, **kwargs):
super(FolderForm, self).__init__(*args, **kwargs)
if hasattr(self, 'instance') and hasattr(self.instance, 'id'):
self.fields['parent_folder'].queryset = Folder.objects.exclude(id=self.instance.id)
To make sure the user does not select the same instance when filling in the foreign key field, implement a clean_FIELDNAME method in the admin form that rejects that bad value.
In this example, the model is Folder and the foreign key is parent_folder:
from django import forms
from django.contrib import admin
from .models import Folder
class FolderAdminForm(forms.ModelForm):
def clean_parent_folder(self):
if self.cleaned_data["parent_folder"] is None:
return None
if self.cleaned_data["parent_folder"].id == self.instance.id:
raise forms.ValidationError("Invalid parent folder, cannot be itself", code="invalid_parent_folder")
return self.cleaned_data["parent_folder"]
class FolderAdmin(admin.ModelAdmin):
form = FolderAdminForm
admin.site.register(Folder, FolderAdmin)
Edit: Combine my answer with raphv's answer for maximum effectiveness.

Django accessing ManyToMany fields from post_save signal

I have a Django model and I want to modify the object permissions on or just after save. I have tried a few solutions and the post_save signal seemed the best candidate for what I want to do:
class Project(models.Model):
title = models.CharField(max_length=755, default='default')
assigned_to = models.ManyToManyField(
User, default=None, blank=True, null=True
)
created_by = models.ForeignKey(
User,
related_name="%(app_label)s_%(class)s_related"
)
#receiver(post_save, sender=Project)
def assign_project_perms(sender, instance, **kwargs):
print("instance title: "+str(instance.title))
print("instance assigned_to: "+str(instance.assigned_to.all()))
In this case, when a Project is created, the signal fires and I see the title, but an empty list for the assigned_to field.
How can I access the saved assigned_to data following save?
You're not going to. M2Ms are saved after instances are saved and thus there won't be any record at all of the m2m updates. Further issues (even if you solve that) are that you're still in a transaction and querying the DB won't get you m2m with proper states anyways.
The solution is to hook into the m2m_changed signal instead of post_save.
https://docs.djangoproject.com/en/dev/ref/signals/#m2m-changed
Your sender then would be Project.assigned_to.through
If your m2m can be empty (blank=True) you are in a little trouble with m2m_changed, because m2m_changed doesn't fire if m2m wasn't set. You can solve this issue by using post_save and m2m_changed at the same time. But there is one big disadvantage with this method - your code will be executed twice if m2m field isn't empty.
So, you can use transaction's on_commit (Django 1.9+)
Django provides the on_commit() function to register callback
functions that should be executed after a transaction is successfully
committed.
from django.db import transaction
def on_transaction_commit(func):
def inner(*args, **kwargs):
transaction.on_commit(lambda: func(*args, **kwargs))
return inner
#receiver(post_save, sender=SomeModel)
#on_transaction_commit
def my_ultimate_func(sender, **kwargs):
# Do things here
Important note: this approach works only if your code calls save().
post_save signal doesn't fire at all in cases when you call only instance.m2m.add() or instance.m2m.set().
Use transaction on commit!
from django.db import transaction
#receiver(post_save, sender=Project)
def assign_project_perms(sender, instance, **kwargs):
transaction.on_commit(lambda: print("instance assigned_to: "+str(instance.assigned_to.all())))
here is an example about how to use signal with many to many field (post like and post comments models),
and in my example i have :
like model (Intermediary table for User and Post tables) : the user can add 1 record only in Intermediary table for each post , which means (unique_together = ['user_like', 'post_like']) for this type of many to many relations you can use 'm2m_changed' signals ,
comment model (Intermediary table for User and Post tables): the user can add many records in Intermediary table for each post , (without unique_together ), for this i just use 'post_save, post_delete' signals , but you can use also 'pre_save, pre_delete' if you like ,
and here is both usage example :
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
class Post(models.Model):
post_user = models.ForeignKey(User,related_name='post_user_related', on_delete=models.CASCADE)
post_title = models.CharField(max_length=100)
post_description = models.TextField()
post_image = models.ImageField(upload_to='post_dir', null=True, blank=True)
post_created_date = models.DateTimeField(auto_now_add=True)
post_updated_date = models.DateTimeField(auto_now=True)
post_comments = models.ManyToManyField(
User,
through="Comments",
related_name="post_comments"
)
p_like = models.ManyToManyField(
User, blank=True,
through="LikeIntermediary",
related_name="post_like_rel"
)
class LikeIntermediary(models.Model):
user_like = models.ForeignKey(User ,related_name="related_user_like", on_delete=models.CASCADE)
post_like = models.ForeignKey(Post ,related_name="related_post_like", on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.user_like} - {self.post_like} "
class Meta:
unique_together = ['user_like', 'post_like']
#receiver(m2m_changed, sender=LikeIntermediary)
def like_updated_channels(sender, instance, **kwargs):
print('this m2m_changed receiver is called, the instance is post id', instance.id)
class Comments(models.Model):
cmt_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="related_comments_user")
cmt_post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="related_comments_post")
cmt_created_date = models.DateTimeField(auto_now_add=True)
cmt_comment_body = models.TextField()
cmt_created = models.DateTimeField(auto_now_add=True)
cmt_updated = models.DateTimeField(auto_now=True)
#receiver(post_save, sender=Comments)
def comments_updated_channels(sender, instance, created, **kwargs):
print('this post_save receiver is called, the instance post id', instance.cmt_post.id)
#receiver(post_delete, sender=Comments)
def comments_deleted_channels(sender, instance, **kwargs):
print('this post_save receiver is called, the instance post id', instance.cmt_post.id)
notes :
the instance with 'm2m_changed' it is a post object .
the instance with 'post_save and post_delete' it is a comment object
this is just an example , and change it based on your case/requirements.
i hope this helpful

Limit Maximum Choices of ManyToManyField

I'm trying to limit the maximum amount of choices a model record can have in a ManyToManyField.
In this example there is a BlogSite that can be related to Regions. In this example I want to limit the BlogSite to only be able to have 3 regions.
This seems like something that would have been asked/answered before, but after a couple hours of poking around I haven't been able to find anything close. For this project, I'm using Django 1.3.
#models.py
class BlogSite(models.Model):
blog_owner = models.ForeignKey(User)
site_name = models.CharField(max_length=300)
region = models.ManyToManyField('Region', blank=True, null=True)
....
class Region(models.Model):
value = models.CharField(max_length=50)
display_value = models.CharField(max_length=60)
....
Any ideas?
You can override clean method on your BlogSite model
from django.core.exceptions import ValidationError
class BlogSite(models.Model):
blog_owner = models.ForeignKey(User)
site_name = models.CharField(max_length=300)
regions = models.ManyToManyField('Region', blank=True, null=True)
def clean(self, *args, **kwargs):
if self.regions.count() > 3:
raise ValidationError("You can't assign more than three regions")
super(BlogSite, self).clean(*args, **kwargs)
#This will not work cause m2m fields are saved after the model is saved
And if you use django's ModelForm then this error will appear in form's non_field_errors.
EDIT:
M2m fields are saved after the model is saved, so the code above will not work, the correct way you can use m2m_changed signal:
from django.db.models.signals import m2m_changed
from django.core.exceptions import ValidationError
def regions_changed(sender, **kwargs):
if kwargs['instance'].regions.count() > 3:
raise ValidationError("You can't assign more than three regions")
m2m_changed.connect(regions_changed, sender=BlogSite.regions.through)
Give it a try it worked for me.
Working! I have used this and its working properly.
Validation required before saving the data. So you can use code in form
class BlogSiteForm(forms.ModelForm):
def clean_regions(self):
regions = self.cleaned_data['regions']
if len(regions) > 3:
raise forms.ValidationError('You can add maximum 3 regions')
return regions
class Meta:
model = BlogSite
fields = '__all__'