I've successfully created and extended the UserCreationForm to get email, first_name & last_name. It works fine in inserting all values. But I think the password field is not being saved. I'm using the built-in auth object to authenticate the user to login to my site. When the use register in the site and tries to login the password doesn't matches and the user cannot login. When I change the password from admin, I can login successfully. So, can anyone help me?
This is how I've extended UserCreationForm:
from django import forms
from django.contrib.auth.forms import User
from django.contrib.auth.forms import UserCreationForm
class RegistrationForm(UserCreationForm):
email = forms.EmailField(required=True)
first_name = forms.TextInput()
last_name = forms.TextInput()
class Meta:
model = User
fields = ("username", "first_name", "last_name", "email", "password1", "password2")
def save(self, commit=True):
user = super(UserCreationForm, self).save(commit=False)
user.email = self.cleaned_data['email']
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
if commit:
user.save()
return user
Now, I click the form link and I change the password.
The problem
def save(self, commit=True):
user = super(UserCreationForm, self).save(commit=False)
^
|___ super of UserCreationForm is ModelForm!
Your code is calling super of UserCreationForm and not the super of your own form RegistrationForm. (The super of UserCreationForm being ModelForm has no idea how to handle user registration like password saving)
The result is that user.set_password() wasn't being called anywhere
However, I'd say no need to call user.set_password again since your super already does.
UserCreationForm.save() is defined as follows:
def save(self, commit=True):
user = super(UserCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
Source code on github
Solution
You should have something like this
def save(self, commit=True):
# Call save of the super of your own class,
# which is UserCreationForm.save() which calls user.set_password()
user = super(RegistrationForm, self).save(commit=False)
# Add the things your super doesn't do for you
user.email = self.cleaned_data['email']
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
if commit:
user.save()
return user
Related
I have a custom user model, I would like my CustomUser model to have a OneToOne relationship with the Person model; Since I want to first register persons and then create a username for them, since not all registered people need to have a username.
I have tried the code shown below; but what I get is the following error:
Cannot assign "1": "CustomUser.dni" must be a "Person" instance.
But, the ID number is already registered in the Person table
P.S. If anyone has a better suggestion for getting people to register first and then create a username for them when strictly necessary, I'd really appreciate it.
models.py
from .managers import CustomUserManager
# MODEL PERSON
class Person(models.Model):
dni = models.CharField('Número de DNI', max_length=8, unique=True)
...
# MODEL CUSTOMUSER
class CustomUser(AbstractBaseUser, PermissionsMixin):
dni = models.OneToOneField(Person, on_delete=models.CASCADE, unique=True)
email = models.EmailField('Correo electrónico', max_length=355, unique=True)
is_staff = models.BooleanField(default=True)
is_active = models.BooleanField(default=True)
is_superuser = models.BooleanField(default=False)
is_admin = models.BooleanField(default=False)
objects = CustomUserManager()
USERNAME_FIELD = 'dni'
REQUIRED_FIELDS = ['email']
...
managers.py
from django.contrib.auth.models import BaseUserManager
class CustomUserManager(BaseUserManager):
def create_user(self, dni, email, password=None):
if not dni:
raise ValueError('Debe ingresar el número de DNI.')
person = self.model(
dni=dni,
email=self.normalize_email(email),
)
person.set_password(password)
person.save(using=self._db)
return person
def create_superuser(self, dni, email, password=None):
person = self.create_user(
dni,
password=password,
email=email,
)
person.is_admin = True
person.is_superuser = True
person.save(using=self._db)
return person
settings.py
AUTH_USER_MODEL = 'accounts.CustomUser'
views.py
from accounts.forms import RegisterForm
class RegisterView(View):
form_class = RegisterForm
initial = {'key': 'value'}
template_name = 'accounts/register.html'
def get(self, request, *args, **kwargs):
form = self.form_class(initial=self.initial)
return render(request, self.template_name, {'form': form})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
form.save()
username = form.cleaned_data.get('username')
messages.success(request, f'Cuenta creada para exitosamente.')
return redirect(to='/')
return render(request, self.template_name, {'form': form})
I want to register a person in the Person table.
After creating the person, you should only be able to create their username and password, from the administration site; that is, an external person cannot register on the site, because the site will have many people in the database, but only some of them will be users of the system.
I hope this image helps clarify the idea.
Process flow
What you're probably looking to do is subclass the django registration form. Then you can change the fields that are required.
I believe the password field would be required by default so a NULL value would not be valid. However, you could quite easily set the password as a randomly generated UUID and then manually change it in the admin panel.
My forms.py is this.
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm,UserChangeForm
class RegistrationForm(UserCreationForm):
email= forms.EmailField(required = True)
class Meta:
model = User
fields = {
'username',
'first_name',
'last_name',
'email',
'password1',
'password2'
}
def save(self, commit=True):
user = super(RegistrationForm, self).save(commit=False)
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
user.email = self.cleaned_data['email']
if commit:
user.save()
And, i have to add "upload image" option for the user so that user could upload their image from any file and load it on the profile.
You have to extend your user model with field profile_image = models.FileField() there is tutorial how to do it.
Than you have to add
class RegistrationForm(UserCreationForm):
email= forms.EmailField(required = True)
profile_image = forms.FileField() # add this field
...
def save(self, commit=True):
...
user.profile_image = self.cleaned_data.get('profile_image')
...
and add enctype="multipart/form-data" attribute to the <form ...> tag in the html template. Here is tutorial from Django docs on File Uploads.
EDIT
Extending the User model in the most basic way without changing the actual User model is that you'll create new model with one-to-one relation to the user model.
from django.conf import settings
class Profile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
profile_image = models.FileField() # where is stored the user profile image
this is the Profile of each user. You will have to create profile when user is created, so:
place this code to the any models.py or file that is loaded on the startup with django
from django.contrib import auth
auth.models.User.add_to_class('create_profile', create_profile)
then create the method below
from (appname).models import Profile
def create_profile(self, **kwargs):
Profile.objects.create(
user=self,
**kwargs # you can pass other fields values upon creating
)
then modify to the RegistrationForm as I wrote before and modify the save function
def save(self, commit=True):
user = super(RegistrationForm, self).save(commit=False)
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
user.email = self.cleaned_data['email']
# user has to be saved to add profile
user.save()
user.create_profile()
user.profile_img = self.cleaned_data.get('profile_image')
user.profile.save()
if commit:
user.save()
also when you are instantiating the RegistrationForm with request.POST do it like
form = RegistrationForm(request.POST, request.FILES)
# to the files have to be passed also, they are not in request.POST
if form.is_valid():
form.save()
and thats it.
I have the following error:
AttributeError: 'User' object has no attribute 'set_password'
The problem is I didn't override the class User:
My model.py:
class User(models.Model):
username = models.CharField(max_length=30)
password = models.CharField(max_length=30)
email = models.EmailField()
def __str__(self):
return self.username
My view.py:
def post(self, request):
form = self.form_class(request.POST)
if form.is_valid():
user = form.save(commit=False)
print type(user)
# Cleaning and normalizing data
username = form.cleaned_data['username']
password = form.cleaned_data['password']
user.set_password(password)
user.save()
# returns User objects if the credential are correct
user = authenticate(username=username, password=password)
if user is not None:
if user.is_active:
login(request, user)
return redirect('website:home')
return render(request, self.template_name, {'form': form})
And this is my form.py:
class UserForm(forms.ModelForm):
password = forms.CharField(widget=forms.TextInput(attrs={'class': 'form-control',
'type': 'password',
'placeholder': 'Enter your password'}))
class Meta:
model = models.User
I don't really know also if I should override the User class. In which case I should and in which case I shouldn't?
You need to inherit from AbstractUser to get access to set_password attribute. Instead of using models.Model use:
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
...
Your User model is not the same as django's User model.
Reference custom user model
from django.contrib.auth.hashers import make_password
replace
user.set_password(password) by user.password = make_password('password')
it clear and work for me.
The User model in Django has .set_password but if you made your own you should try OneToOneField(User) from there you just have to make sure you save both in the views.
user_form = UserForm(data=request.POST)
if user_form.is_valid():
user = user_form.save()
user.set_password(user.password)
profile = user.userprofile
profile.bio = request.POST['bio']
profile.save()
Below is the content of my forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
class RegistrationForm(UserCreationForm):
email = forms.EmailField(required=True)
first_name = forms.CharField(max_length=20, required=True)
last_name = forms.CharField(max_length=20, required=True)
class Meta:
model = User
fields = ('username', 'email', 'password1', 'password2', 'first_name', 'last_name')
def save(self, commit=True):
user = super(UserCreationForm, self).save(commit=False)
user.email = self.cleaned_data['email']
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
#validation of email id----------
email1 = user.email
(fisrt, second,) = email1.split("#")
(domain, exn,) = second.split(".")
if not domain == "tmail":
raise forms.ValidationError("Domain must be 'tmail'")
if commit:
user.save()
return user
I am able to print form (including fields email, first_name & last_name) & register the user successfully only issue I have is, Its not performing the validation step. (code below the "#validation of email id----------")
Even if the domain is other than "tmail" it is accepting it without raising any validation error & then registering the user into database. Please help & let me know in case you need more info.
It is too late to validate the email in the save() method. Django lets you validate individual fields by defining a method clean_<fieldname>. This will be called when the form is validated.
def clean_email(self):
email = self.cleaned_data['email']
(first, second,) = email1.split("#")
(domain, exn,) = second.split(".")
if domain != "tmail":
raise forms.ValidationError("Domain must be 'tmail'")
return email
See the docs on cleaning a specific field attribute for more info.
Why are you doing validation in save? All validations must happen before save, such that if any exception does comes in save it is most probably bug in your code. Forms specifically have 'clean' methods especially for this purpose. For specific field validation, you can use "clean_{field_name}". Though if your validation depends on multiple fields, you need to use "clean" method.
I ran today into a special situation. Previously I had the following in my view.py
def register_page(request):
if request.method == 'POST':
form = RegistrationForm(request.POST)
if form.is_valid():
user = User.objects.create_user(
username=form.cleaned_data['username'],
password=form.cleaned_data['password2'],
email=form.cleaned_data['email']
)
return HttpResponseRedirect('/register/success/')
else:
form = RegistrationForm()
variables = RequestContext(request, {'form':form})
return render_to_response('registration/register.html', variables)
It was pretty straight forward retrieving the username, email and password to create a new user after she has registered. But now I have refactored it to use a hash code as the username and utilize the email alone to register and login.
The shortened RegistrationForm looks like this:
class RegistrationForm(forms.ModelForm):
email = forms.EmailField(label=_("Email"))
password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput))
class Meta:
model = User
fields = ("email",)
def save(self, commit=True):
user = super(RegistrationForm, self).save(commit=False)
user.set_password(self.cleaned_data["password1"])
email = self.cleaned_data['email']
user.username = md5(email).digest().encode('base64')[:-1]
if commit:
user.save()
return user
The new form doesn't have the username any longer, since it is calculated and not entered by the user any more. But how do I retrieve the username from the view ? The new code is not from me and I have it from a blog. Maybe the key is here in the Meta class? From the documentation I wasn't able to fully understood what he is trying to achieve with the Meta class here...
Many Thanks,
EDIT:
Ok I think I understand now how the subclassing should work. I tried to subclass the User class like this:
class cb_user_model_backend(ModelBackend):
def create_user(self, email=None, password=None):
"""
Creates and saves a User with the given email and password only.
"""
now = timezone.now()
username = md5(email).digest().encode('base64')[:-1]
email = UserManager.normalize_email(email)
user = self.model(username=username, email=email,
is_staff=False, is_active=True, is_superuser=False,
last_login=now, date_joined=now)
user.set_password(password)
user.save(using=self._db)
return user
The problem I am facing now are two errors, self._db and self.model, were meant to be on the base user class. How do get to them from here?
Edit 2:
PyCharm complains that the two self._db and seld.model don't exit on current cb_user_model_backend.
Note the View is refactored to take two parameters:
user = User.objects.create_user(
password=form.cleaned_data['password2'],
email=form.cleaned_data['email']
)
When running it stack trace is:
Exception Type: TypeError
Exception Value:
create_user() takes at least 2 arguments (3 given)
Try subclassing your save method in your models.py:
def save(self, *args, **kwargs):
if not self.id:
self.username = md5(self.email).digest().encode('base64')[:-1]
super(ModelName, self).save(*args, **kwargs)
After calling user.save(), user.username should yield the generated username in your views. Hope this helps.
EDIT:
If you want to call create_user(**kwargs), you could do the following in your views.py:
email = self.cleaned_data['email']
username = md5(email).digest().encode('base64')[:-1]
u = User.objects.create_user(username = username, email = email, password = password)