How to save data into two related tables in Django?
I'm new to Django and I'm following a tutorial to create a school system. I have a CustomUser
that is related to three different tables by a OneToOneField()
, when registering a user, depending on the type (HOD, teacher, student), I create two signals with @receiver
to create an instance of the POST
request and then save it in its respective table.
Problem:
- When I try to save the data, I use a
CustomUser
object and call thecreate_user()
class that I save in a user variable, but when I use that object to save other data from the table I need (Teachers in this case) the user is not created:
def create_teachers(request):
if auth_user(request) == True:
# Do something for authenticated users.
if request.method == 'GET':
return render(request, 'create_teachers.html')
else:
try:
user = CustomUser.objects.create_user(
password='defaultPass',
first_name=request.POST.get('first_name'),
last_name=request.POST.get('last_name'),
email=request.POST.get('email'),
user_type=2,
)
user.Teachers.address=request.POST.get('address')
user.save()
print('Professor created successfully')
return redirect('/auth/create_teachers')
except:
print('Error creating teacher')
return redirect('/auth/create_teachers')
else:
return redirect('../login')
Error creating teacher
[27/Sep/2024 13:46:14] "POST /auth/create_teachers HTTP/1.1" 302 0
[27/Sep/2024 13:46:14] "GET /auth/create_teachers HTTP/1.1" 200 34724
[27/Sep/2024 13:46:14] "GET /static/css/dist/styles.css?v=1727459174 HTTP/1.1" 200 54543
# admin.py
class UserAdmin(BaseUserAdmin):
ordering = ('email',)
admin.site.register(CustomUser, UserAdmin)
# managers.py
class CustomUserManager(BaseUserManager):
"""
Custom user model manager where email is the unique identifiers
for authentication instead of usernames.
"""
def create_user(self, email, password, **extra_fields):
"""
Create and save a user with the given email and password.
"""
if not email:
raise ValueError(_("The Email must be set"))
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password, **extra_fields):
"""
Create and save a SuperUser with the given email and password.
"""
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get("is_staff") is not True:
raise ValueError(_("Superuser must have is_staff=True."))
if extra_fields.get("is_superuser") is not True:
raise ValueError(_("Superuser must have is_superuser=True."))
return self.create_user(email, password, **extra_fields)
# models.py
class CustomUser(AbstractUser):
username = None
email = models.EmailField(_("email address"), unique=True)
user_type_data=((1,'HOD'),(2,'staff'),(3,'users'))
user_type=models.CharField(default=1,choices=user_type_data,max_length=10)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
objects = CustomUserManager()
def __str__(self):
return self.email
class AdminHOD(models.Model):
id=models.AutoField(primary_key=True)
admin=models.OneToOneField(CustomUser,on_delete=models.CASCADE)
created_at=models.DateTimeField(auto_now_add=True)
updated_at=models.DateTimeField(auto_now_add=True)
objects=models.Manager()
class Teachers(models.Model):
id=models.AutoField(primary_key=True)
admin=models.OneToOneField(CustomUser,on_delete=models.CASCADE)
phone=PhoneNumberField()
address=models.TextField()
course_id=models.ManyToManyField(Courses)
subject_id=models.ManyToManyField(Subjects)
section_id=models.ManyToManyField(Sections)
created_at=models.DateTimeField(auto_now_add=True)
updated_at=models.DateTimeField(auto_now_add=True)
class Students(models.Model):
id=models.AutoField(primary_key=True)
admin=models.OneToOneField(CustomUser,on_delete=models.CASCADE)
idCard=models.PositiveIntegerField(unique=True)
age=models.PositiveSmallIntegerField()
gender=models.CharField(max_length=255,)
phone=PhoneNumberField()
address=models.TextField()
profile_pic=models.FileField()
session_start=models.DateField()
session_end=models.DateField()
course_id=models.ForeignKey(Courses,on_delete=models.SET_DEFAULT, default=1)
sections_id=models.ForeignKey(Sections,on_delete=models.SET_DEFAULT, default=1)
created_at=models.DateTimeField(auto_now_add=True)
updated_at=models.DateTimeField(auto_now_add=True)
# Receive a signal to create a new user with certain type
@receiver(post_save,sender=CustomUser)
def create_user_profile(sender,instance,created,**kwargs):
if created:
if instance.user_type == 1:
AdminHOD.objects.create(admin=instance)
if instance.user_type == 2:
Teachers.objects.create(admin=instance)
if instance.user_type == 3:
Students.objects.create(admin=instance)
# Save the user
@receiver(post_save,sender=CustomUser)
def save_user_profile(sender,instance,**kwargs):
if instance.user_type == 1:
instance.adminhod.save()
if instance.user_type == 2:
instance.teachers.save()
if instance.user_type == 3:
instance.students.save()
If I comment out the line user.Teachers.address=request.POST.get('address')
, the user is created without problems and the instance in the Teachers
table is created with the address field empty.
What I need is to understand why the error occurs and how I can fix it. I had thought about creating a custom model for each table instead of using a CustomUser
and referencing the other tables afterwards, but I don't know how optimal that solution is. Any advice to improve the code is also welcome. Thanks
EDIT: Traceback.
For my answer, I'm renaming the Teachers
model to Teacher
, because using plural table names gets quite confusing in this instance.
To access a related model field, you need to use the field's related_name
:
# the default related name for a `OneToOneField` is the lowercase name of the model
class Teacher(models.Model):
id=models.AutoField(primary_key=True)
admin=models.OneToOneField(
CustomUser,
on_delete=models.CASCADE,
related_name='teacher'
)
...
Now in your view, you can create the related Teacher
object using the related_name
:
user.teacher.address=request.POST.get('address')
user.teacher.save()
You're trying to set attributes on a related model (user.Teachers.address
) without first creating an instance of that model.
When you create a CustomUser
instance, the Teachers
instance is automatically created by the signal defined in your code. However, the Teachers
instance is not accessible directly via user.Teachers
until it has been saved and is properly linked.
The line user.Teachers.address=request.POST.get('address')
fails because user.Teachers
doesn't yet exist in the way you're trying to use it.
Instead of trying to set the address directly on the Teachers
instance via the CustomUser
, you should create or retrieve the Teachers
instance after the CustomUser
has been saved.
def create_teachers(request):
if auth_user(request) == True:
if request.method == 'GET':
return render(request, 'create_teachers.html')
else:
try:
user = CustomUser.objects.create_user(
password='defaultPass',
first_name=request.POST.get('first_name'),
last_name=request.POST.get('last_name'),
email=request.POST.get('email'),
user_type=2,
)
# Now retrieve the Teachers instance created by the signal
teacher_instance = Teachers.objects.get(admin=user)
teacher_instance.address = request.POST.get('address')
teacher_instance.save()
print('Professor created successfully')
return redirect('/auth/create_teachers')
except Exception as e:
print(f'Error creating teacher: {e}')
return redirect('/auth/create_teachers')
else:
return redirect('../login')
First thing first as I see your CustomUser class doesn't have any Teachers attribute or field. You have a CustomUser with the below attributes:
class CustomUser(AbstractUser):
username = None
email = models.EmailField(_("email address"), unique=True)
user_type_data=((1,'HOD'),(2,'staff'),(3,'users'))
user_type=models.CharField(default=1,choices=user_type_data,max_length=10)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
Where is the Teachers attribute in user instance object that you call in the below code:
user.Teachers.address=request.POST.get('address')
As we see in the code you have Teachers class (just below AdminHOD class and above Students class) which is independent to CustomUser user class in your code, but no Teachers attribute.
So you call something from user (I mean the instance) that does not exist.
It has been declared in traceback image you posted: 'CustomUser' object has no attribute 'Teachers'
.
It may be better to save user first then edit the linked teacher or make a Teacher attribute in CustomUser class.