Search code examples
pythondjangodjango-formsdjango-allauth

Django-allauth - Custom Sign Up with OneToOneField


I created a sign up form using two grouped forms and it has been working perfectly, but I would like to use django-allauth because of the features (login only with e-mail, sending confirmation e-mail ...). However even reading some topics I still couldn't.

forms.py

class ExtendedUserCreationForm(UserCreationForm):
    email = forms.EmailField(required=True, label="E-mail")
    first_name = forms.CharField(max_length=30, label="Nome")
    last_name = forms.CharField(max_length=30, label="Sobrenome")


    class Meta:
        model = User
        fields = ('first_name', 'last_name', 'username', 'email', 'password1', 'password2')


    def save(self, commit=True):
        user = super().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


class UserProfileForm(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = ('sexo', 'data_nascimento', 'foto', 'sobre_mim', 'telefone', 'paroquia',
                  'cidade','estado', 'cep', 'possui_filhos', 'facebook', 'instagram')
        CIDADES = []
        for i in cidadesReader:
            if i[1] not in CIDADES:
                CIDADES.append(i[1])
        widgets = {            
            'cidade': floppyforms.widgets.Input(datalist=CIDADES, attrs={'autocomplete': 'off'}),            
        }

views.py

def signup(request):
    if request.method == 'POST':
        form = ExtendedUserCreationForm(request.POST)
        profile_form = UserProfileForm(request.POST, request.FILES)

        if form.is_valid() and profile_form.is_valid():

            user = form.save()

            profile = profile_form.save(commit=False)
            profile.user = user


            profile.save()

            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password1')
            user = authenticate(username=username, password=password)
            #login(request, user)
            return redirect('home')
    else:
        form = ExtendedUserCreationForm()
        profile_form = UserProfileForm()

    context = {'form': form, 'profile_form' : profile_form}
    return render(request, 'registration/signup.html', context)

signup.html

{% extends '_base.html' %}

{% load crispy_forms_tags %}

{% block title %}Cadastrar{% endblock title %}

{% block content %}

<h2>Criar Perfil</h2>
<form novalidate method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form|crispy }}
    {{ profile_form|crispy }}
    <button class="btn btn-success" type="submit">Cadastrar</button>
</form>
{% endblock content %}

models.py

class UserProfile(models.Model):
        user = models.OneToOneField(User, on_delete=models.CASCADE)    
    
    
        SEXOS = (
            ('M', 'Masculino'),
            ('F', 'Feminino'),
        )
        sexo = models.CharField(max_length=1, choices=SEXOS)
        data_nascimento = models.DateField(validators=[idade_minima])    
        ...

I've tried using the ACCOUNT_SIGNUP_FORM_CLASS and ACCOUNT_FORMS options in settings.py, but it didn't work.

I tried to make some adjustments, as in this topic similar to my question: Django allauth saving custom user profile fields with signup form

For example, I changed it in models.py and I did migrate:

user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True, related_name ='profile')

After several attempts, the most common error is:

RelatedObjectDoesNotExist at /accounts/signup/

User has no profile.

Edit:

I changed my slug in UserProfile, because it depends from user (first name). The error changed:

IntegrityError at /accounts/signup/

NOT NULL constraint failed: profiles_userprofile.user_id

But UserProfile has no user continues in the final. (Using in settings.py: ACCOUNT_SIGNUP_FORM_CLASS = 'profiles.forms.UserProfileForm'. Details from traceback:

...lib/python3.6/site-packages/allauth/account/views.py in dispatch
215 return super(SignupView, self).dispatch(request, *args, **kwargs)

.../lib/python3.6/site-packages/allauth/account/views.py in post
104 response = self.form_valid(form)

...lib/python3.6/site-packages/allauth/account/views.py in form_valid
231 self.user = form.save(self.request)

...lib/python3.6/site-packages/allauth/account/forms.py in save
405 self.custom_signup(request, user)

...lib/python3.6/site-packages/allauth/account/forms.py in custom_signup
359 custom_form.save(user)

...profiles/models.py in save
super(UserProfile, self).save(*args, **kwargs)

 ▼ Local vars
Variable    Value
__class__   

<class 'profiles.models.UserProfile'>

args    ()
kwargs  {}
self    Error in formatting: RelatedObjectDoesNotExist: UserProfile has no user.
slug_name   'nome-sp-260221205510' 

Signals

Using signals the error changed. I added it in models.py:

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        UserProfile.objects.create(user=instance)

Error:

ValueError at /accounts/signup/
The 'foto' attribute has no file associated with it.

Then I tried remove foto field, but the other error happens in another field:

IntegrityError at /accounts/signup/
NOT NULL constraint failed: profiles_userprofile.data_nascimento

Thanks in advance for any help.


Solution

  • I achieved! No need to use signals. Here are the changes:

    forms.py

    I needed to use a single class:

    class SignupForm(forms.ModelForm):
    
        first_name = forms.CharField(max_length=30, label="Nome")
        last_name = forms.CharField(max_length=30, label="Sobrenome")
    
    
        class Meta:
            model = UserProfile
    
            fields = ('sexo', 'data_nascimento', 'foto', 'sobre_mim','telefone','paroquia',
                      'cidade','estado', 'cep', 'possui_filhos', 'facebook', 'instagram')
    
            CIDADES = []
            for i in cidadesReader:
                if i[1] not in CIDADES:
                    CIDADES.append(i[1])
            widgets = {
                'cidade': floppyforms.widgets.Input(datalist=CIDADES, attrs={'autocomplete': 'off'}),
            }
    
        field_order = ['first_name', 'last_name', 'email', 'password1', 'password2',
                   'sexo', 'data_nascimento', 'foto', 'sobre_mim','telefone','paroquia',
                    'cidade','estado', 'cep', 'possui_filhos', 'facebook', 'instagram']
    
    
        def signup(self, request, user):
            user.first_name = self.cleaned_data['first_name']
            user.last_name = self.cleaned_data['last_name']
            profile, created = models.UserProfile.objects.get_or_create(user=user)
            profile.sexo = self.cleaned_data['sexo']
            profile.data_nascimento = self.cleaned_data['data_nascimento']
    
    
            def compressImage(foto):
                ... 
                return foto
    
    
            profile.foto = compressImage (self.cleaned_data['foto'])
            profile.sobre_mim = self.cleaned_data['sobre_mim']
            profile.telefone = self.cleaned_data['telefone']
            profile.paroquia = self.cleaned_data['paroquia']
            profile.cidade = self.cleaned_data['cidade']
            profile.estado = self.cleaned_data['estado']
            profile.cep = self.cleaned_data['cep']
            profile.possui_filhos = self.cleaned_data['possui_filhos']
            profile.facebook = self.cleaned_data['facebook']
            profile.instagram = self.cleaned_data['instagram']
            user.save()
            profile.save()
    

    Note:

    I was using a function to compress images, in models.py. To correct the error

    ValueError at /accounts/signup/
    The 'foto' attribute has no file associated with it
    

    I had to bring it to forms.py

    settings.py

    ACCOUNT_SIGNUP_FORM_CLASS = 'profiles.forms.SignupForm'

    models.py

    class UserProfile(models.Model):
        user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True, related_name ='profile')
    
    
        SEXOS = (
            ('M', 'Masculino'),
            ('F', 'Feminino'),
        )
        sexo = models.CharField(max_length=1, choices=SEXOS)
        ...
    

    Note:

    It was necessary to test field by field. Sometimes there were some errors like NOT NULL constraint failed ou no such table. The solutions to these problems:

    • Add null=True in the field (temporarily)
    • makemigrations and migrate
    • Delete migrations

    signup.html

    Only {{ form|crispy }} is necessary (I could delete {{ profile_form|crispy }})

    <form novalidate method="post" enctype="multipart/form-data">
        {% csrf_token %}
        {{ form|crispy }}
        <button class="btn btn-success" type="submit">Cadastrar</button>
    </form>
    

    Thank you for your help, @Risadinha.