I am trying to create multi user registration system with Django. However, anytime I call the save() method to save a User type, it saves into the User table twice. The funny thing about the second model that is saved is that many required fields are empty.
I am using a custom user model that I created from AbstractBaseUser. I also rewrote the forms for the CustomUser model. For the multiple user types, I am using a profile model (Student model has a OneToOne field to the user model)
models.py:
class User(AbstractBaseUser, PermissionsMixin):
# I've removed some unimportant code here
email = models.EmailField(
verbose_name='email address',
max_length=255,
unique=True,
)
class Types(models.TextChoices):
STUDENT = 'STUDENT', 'Student'
DEPARTMENT_USER = 'DEPARTMENT_USER', 'Department user'
ADMIN = 'ADMIN', 'Admin'
user_type = models.CharField(_('Type'), max_length=50, choices=Types.choices, default=Types.STUDENT)
first_name = models.CharField(_('First name'), max_length=70, blank=False, default="")
middle_name = models.CharField(_('Middle name'), max_length=70, blank=True, default="")
last_name = models.CharField(_('Last name'), max_length=70, blank=False, default="")
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False) # a admin user; non super-user
is_superuser = models.BooleanField(default=False) # a superuser
last_login = models.DateTimeField(null=True, blank=True)
date_joined = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = 'email'
EMAIL_FIELD = 'email'
REQUIRED_FIELDS = ['user_type', 'first_name', 'last_name'] # Email & Password are required by default.
objects = UserManager()
class Meta:
verbose_name = ('user')
verbose_name_plural = ('users')
#db_table = 'auth_user'
abstract = False
class AccountConfirmed(models.Model):
# Model to determine which users have confirmed their email addresses.
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='accountconfirmed')
email_confirmed = models.BooleanField(default=False)
reset_password = models.BooleanField(default=False)
class Meta:
app_label = 'auth'
# When the user model is created, through signals an AccountConfirmed model is also created.
# The email_confirmed and reset_password field is set to false.
#receiver(models.signals.post_save, sender=User)
def update_user_profile(sender, instance, created, **kwargs):
if created:
AccountConfirmed.objects.create(user=instance)
instance.accountconfirmed.save()
######################################################
######################################################
class Student(User):
# This is the model class for students
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True, related_name='students')
matric_number = models.CharField(_('Matriculation number'), max_length=11, blank=False)
department = models.CharField(_('Department'), max_length=40, blank=False)
# has_graduated, level, etc. future possibilities
def __str__(self):
return f'{self.user.email}'
forms.py:
class StudentSignupForm(UserCreationForm):
# first_name = forms.CharField(max_length=70)
# middle_name = forms.CharField(max_length=70, required=False)
# last_name = forms.CharField(max_length=70)
matric_number = forms.CharField(min_length=10, max_length=11, help_text='Your Matric number must be 10 characters')
department = forms.CharField(max_length=40, help_text='e.g Computer Science')
class Meta(UserCreationForm.Meta):
model = User
fields = UserCreationForm.Meta.fields + ('matric_number', 'department')
#transaction.atomic
def save(self, commit=True):
# Save the User instance and get a reference to it
user = super().save(commit=False)
user.user_type = User.Types.STUDENT
user.is_active = False
#if commit:
user.save()
print(f' forms.py {user.email} {user.first_name}')
student = Student.objects.create(user=user, matric_number=self.cleaned_data.get('matric_number'), department=self.cleaned_data.get('department'))
# Add other details
# Return User instance, not Student instance
return user
views.py:
class StudentUserSignupView(CreateView):
model = User
template_name = 'account/signup.html'
form_class = StudentSignupForm
def get_context_data(self, **kwargs):
kwargs['user_type'] = 'STUDENT'
return super().get_context_data(**kwargs)
def form_valid(self, form):
user = form.save()
#login(self.request, user)
send_verification_mail(self, user)
return redirect('verification_sent')
Anytime a user signs up, this is what the students table looks like:
Also, this is what the users table look like after signup (with the multiple saves)
So how do I correct the multiple saves in the user table?
Also, How is it even possible to save a model with most of the required fields empty?
As pointed out by #RaghavKundra, the line below was what caused the problem of saving multiple times to the database
class Student(User):
Instead of that, it should be
class Student(models.Model):
Related
I have a User model and Employee model which stands for additional info about user, it has one to one field relation with User instance. When new User is created I use signals to create new Employee instance. Now I added new field to User model called 'fk_employee_id' just to have a link to this newly created Employee and I'm not sure how to do pass it's id to this User's fk field. I tried to write in my signals something like instance.user.fk_employee_id = sender after that I get
ValueError Cannot assign "<class 'employees.models.Employees'>": "User.fk_employee_id" must be a "Employees" instance.
So how do I fill this foreign key field in User instance when Employee is created?
My User model:
class User(AbstractBaseUser):
email = models.EmailField(verbose_name='email', max_length=60, unique=True)
username = models.CharField(max_length=30, unique=True, validators=[validate_username])
date_joined = models.DateTimeField(verbose_name='date joined', auto_now_add=True)
last_login = models.DateTimeField(verbose_name='last login', auto_now=True)
is_admin = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=50)
fk_employee_id = models.OneToOneField('employees.Employees', related_name='fk_employee_id',
null=True,on_delete=models.DO_NOTHING)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'first_name', 'last_name']
objects = UserManager()
The Employee model is large, it has basic fields like number, address etc. Nothing special.
My signals file in users app:
from django.db.models.signals import post_save
from .models import User
from django.dispatch import receiver
from employees.models import Employees
#receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Employees.objects.create(user=instance, first_name=instance.first_name,
last_name=instance.last_name)
#receiver(post_save, sender=Employees)
def save_profile(sender, instance, **kwargs):
instance.user.save()
You don't need 2 OneToOneFields.
One for the Employee is enough.
Let's say you have:
class Employee(models.Model):
user = models.OneToOneField(User, related_name='employee_profile',on_delete=models.DO_NOTHING)
...
After migration, you can just call user.employee_profile (like in the related_name attribute).
I am new to django and I created this "apply now form" exclusively for tutors that when they submit the form it will appear to the admin site, and I will manually check it if they are a valid tutor. And if they are a valid tutor, I will check the is_validated booleanfield in the admin site to the corresponding tutor that sent the form, so that he/she will have access to other things in the site. But I am having this problem that when you submit the form this comes up..
NOT NULL constraint failed: account_tutorvalidator.user_id
I have search for some solutions and also read similar questions here but I still couldn't understand what to do.. could someone help me out with this?
here is my models.py
class User(AbstractUser):
is_student = models.BooleanField(default=False)
is_tutor = models.BooleanField(default=False)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
phone_number = models.CharField(max_length=11, blank=False, null=True)
current_address = models.CharField(max_length=100, null=True)
image = models.ImageField(default='default-pic.jpg', upload_to='profile_pics')
def __str__(self):
return f'{self.first_name} {self.last_name}'
class TutorProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, null=True,
related_name='tutor_profile')
bio = models.CharField(max_length=255, blank=True)
is_validated = models.BooleanField(default=False)
def __str__(self):
return f"{self.user.first_name} {self.user.last_name}'s Profile"
class TutorValidator(models.Model):
user = models.ForeignKey(TutorProfile, on_delete=models.CASCADE)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
dbs = models.ImageField(upload_to='dbs_pics')
driving_license = models.ImageField(upload_to='drivers_license_pics', null=True, blank=True)
national_id = models.ImageField(upload_to='national_id_pics', null=True, blank=True)
def __str__(self):
return f"{self.first_name}'s application form"
my forms.py
class TutorValidationForm(forms.ModelForm):
class Meta:
model = TutorValidator
fields = ['first_name', 'last_name', 'driving_license', 'national_id']
labels = {
'national_id': _('National ID')
}
my views.py
class TutorValidatorView(LoginRequiredMixin, FormView):
template_name = 'account/tutor_validator.html'
form_class = TutorValidationForm
success_url = '/'
The error is because TutorValidator requires that you set the user profile foreign key which your form currently does not support, so you need a way to set this to the object you are creating, and use the current logged in user (the one who is submitting the form).
You can do this by overriding form_valid. Try with:
class TutorValidatorView(LoginRequiredMixin, FormView):
...
def form_valid(self, form):
tutor_validator = form.save(commit=False)
tutor_validator.user = self.request.user.tutor_profile
tutor_validator.save()
return HttpResponseRedirect(self.get_success_url())
Note that the current user needs to already have an existing TutorProfile. Otherwise you need to create that first to connect it to TutorValidator
When I run api request I get a following error:
AttributeError: Got AttributeError when attempting to get a value for field email on serializer UserSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the tuple instance.
Original exception text was: 'tuple' object has no attribute 'email'.
New user gets inserted in database anyway, email field is filleld properly.
View:
class Register(APIView):
def post(self, request):
serializer = UserSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
if user:
return Response(serializer.data, status=status.HTTP_201_CREATED)
Serializer:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['email', 'username', 'name', 'password']
def create(self, validated_data):
user = User.objects.create_user(**validated_data),
return user
Model:
class User(AbstractBaseUser):
email = models.EmailField(max_length=254, unique=True)
username = models.CharField(max_length=30, unique=True)
name = models.CharField(max_length=60)
date_of_birth = models.DateField(blank=True, null=True)
bio = models.CharField(default='', max_length=10000)
photo = models.ImageField(max_length=255, null=True, blank=True)
email_verified_at = models.DateTimeField(null=True, blank=True)
email_token_time = models.DateTimeField(null=True, blank=True)
email_token = models.CharField(default='', max_length=64)
password_token_time = models.DateTimeField(null=True, blank=True)
password_token = models.CharField(default='', max_length=64)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
last_seen = models.DateTimeField(null=True, blank=True)
USERNAME_FIELD = 'email'
EMAIL_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'name']
objects = UserManager()
class Meta:
db_table = "User"
def __str__(self):
return self.username
I also have custom user manager, but that is probably irrelevant, and works as user does get inserted to database.
You have typo in this line:
user = User.objects.create_user(**validated_data),
It contains comma , in the last of line. So user become a tuple of user instance, not just user instance. It become (user,).
Should return user instance.
I've set up my models, serializers and viewsets in my Django REST API to assign a search record to a particular user, and to associate all the relevant user's searches to their record in the User model. It was all working fine, but I'm now getting the TypeError error message (in the subject line of this question) when I try to create a new user. I've listed the relevant models, serializers and viewsets below. Please could anyone take a look and let me know where I'm going wrong? Any help would be very much appreciated.
User serializer:
class UserSerializer(serializers.ModelSerializer):
searches = serializers.PrimaryKeyRelatedField(many=True, queryset=SearchHistoryModel.objects.all())
class Meta:
model = User
fields = ('id', 'username', 'email', 'password', 'searches')
extra_kwargs = {'email': {
'required': True,
'validators': [UniqueValidator(queryset=User.objects.all())]
}}
def create(self, validated_data):
user = User.objects.create_user(**validated_data)
Token.objects.create(user=user)
return user
User viewset:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.AllowAny]
Search model:
class SearchHistoryModel(models.Model):
"""
Stores each user's search submission
"""
created_date = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(User, related_name='searches', on_delete=models.CASCADE)
cpu_component_name = models.CharField(max_length=10, blank=False)
cpu_subcomponent_name = models.CharField(max_length=50, blank=False)
motherboard_name = models.CharField(max_length=20, blank=False)
gpu_component_name = models.CharField(max_length=10, blank=True, null=True)
gpu_subcomponent_name = models.CharField(max_length=50, blank=True, null=True)
gpu_subcomponent_quantity = models.PositiveIntegerField(default=0)
ram_component_name = models.CharField(max_length=15, blank=True, null=True)
ram_component_quantity = models.PositiveIntegerField(default=0)
ssd_component_name = models.CharField(max_length=15, blank=True, null=True)
ssd_component_quantity = models.PositiveIntegerField(default=0)
hdd_component_name = models.CharField(max_length=20, blank=True, null=True)
hdd_component_quantity = models.PositiveIntegerField(default=0)
optical_drive_name = models.CharField(max_length=15, blank=True, null=True)
class Meta:
verbose_name = 'Search'
verbose_name_plural = 'Searches'
ordering = ['owner', 'created_date']
def __str__(self):
return '{}\'s search choices'.format(self.owner)
Search serializer:
class SearchHistorySerializer(serializers.ModelSerializer):
"""
Serializes the user's search history data passed into the SearchHistoryModel
Associates each search with the relevant user
"""
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = SearchHistoryModel
fields = (
'id', 'created_date', 'owner', 'cpu_component_name', 'cpu_subcomponent_name',
'motherboard_name', 'gpu_component_name', 'gpu_subcomponent_name',
'gpu_subcomponent_quantity', 'ram_component_name', 'ram_component_quantity',
'ssd_component_name', 'ssd_component_quantity', 'hdd_component_name',
'hdd_component_quantity', 'optical_drive_name'
)
Search viewset:
class SearchHistoryViewSet(viewsets.ModelViewSet):
queryset = SearchHistoryModel.objects.all()
serializer_class = SearchHistorySerializer
permission_classes = [permissions.IsAuthenticated]
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
In user = User.objects.create_user(**validated_data), validated_data contains a searches value which is an id.
But actually the ForeignKey is in the other sense : in Searches model, and to refer to a User instance, not the opposite.
To link a user to searches, it is not in User DB table that you write an id, but in Searches that you write a User id.
class UserSerializer(serializers.ModelSerializer):
(...)
def create(self, validated_data):
# Extract the value from 'validated_data'
search_ids = validated_data.pop('searches', None)
user = User.objects.create_user(**validated_data)
Token.objects.create(user=user)
# Update existing search instances
for search_id in search_ids:
Search.objects.filter(id=search_id).update(owner=user)
return user
I want to use hashing field set_password from User model in django.contrib.auth.models and I'm currently using a custom User model for that.
I'm getting the following error: Attribute error: 'User' object has no attribute 'set_password'
models.py
from django.db import models
class User(models.Model):
first_name = models.CharField(max_length=50, blank=True)
last_name = models.CharField(max_length=50, blank=True)
profile_picture =
models.ImageField(upload_to="user_data/profile_picture", blank=True)
username = models.CharField(max_length=100)
birth_date = models.DateField(blank=True)
gender = models.CharField(max_length=10, blank=True)
password = models.CharField(max_length=1000)
contact = models.CharField(max_length=10, blank=True)
email = models.CharField(max_length=100)
time_stamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.username
views.py
...
from .models import User
...
def post(self, request):
# Data is here
form = self.form_class(request.POST)
if form.is_valid():
# create object of form
user = form.save(commit=False)
# cleaned/normalised data
username = form.cleaned_data['username']
password = form.cleaned_data['password']
# convert plain password into hashed
user.set_password(user.password)
user.save()
return HttpResponse('Done here.')
...
forms.py (just used a widget in forms.py)
from .models import User
from django import forms
class UserForm(forms.ModelForm):
password = forms.CharField(widget=forms.PasswordInput)
class Meta:
model = User
fields = ['username', 'password']
This is a really easy fix. Just change your models.py file like so:
from django.contrib.auth.models import AbstractBaseUser
class User(AbstractBaseUser):
first_name = models.CharField(max_length=50, blank=True)
last_name = models.CharField(max_length=50, blank=True)
profile_picture = models.ImageField(upload_to="user_data/profile_picture", blank=True)
username = models.CharField(max_length=100)
birth_date = models.DateField(blank=True)
gender = models.CharField(max_length=10, blank=True)
password = models.CharField(max_length=1000)
contact = models.CharField(max_length=10, blank=True)
email = models.CharField(max_length=100)
time_stamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.username
That way, your user model will inherit all of the AbstractBaseUser methods, including set_password.
Look at this full example from the documentation for extra information.