117

I have a django project with the django-allauth app. I need to collect additional data from the user at signup. I came across a similar question here but unfortunately, no one answered the profile customization part.

Per the documentation provided for django-allauth:

ACCOUNT_SIGNUP_FORM_CLASS (=None)

A string pointing to a custom form class (e.g. ‘myapp.forms.SignupForm’) that is used during signup to ask the user for additional input (e.g. newsletter signup, birth date). This class should implement a ‘save’ method, accepting the newly signed up user as its only parameter.

I am new to django and am struggling with this. Can someone provide an example of such a custom form class? Do I need to add a model class as well with a link to the user object like this ?

Community
  • 1
  • 1
Shreyas
  • 1,410
  • 3
  • 11
  • 15

8 Answers8

180

Suppose you want to ask the user for his first/last name during signup. You'll need to put these fields in your own form, like so:

class SignupForm(forms.Form):
    first_name = forms.CharField(max_length=30, label='Voornaam')
    last_name = forms.CharField(max_length=30, label='Achternaam')

    def signup(self, request, user):
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.save()

Then, in your settings point to this form:

ACCOUNT_SIGNUP_FORM_CLASS = 'yourproject.yourapp.forms.SignupForm'

Note that SignupForm cannot be defined in the same file as form overrides through ACCOUNT_FORMS or SOCIALACCOUNT_FORMS, because that would lead to a circular import error.

That's all.

Rok Strniša
  • 6,781
  • 6
  • 41
  • 53
pennersr
  • 6,306
  • 1
  • 30
  • 37
  • 11
    Thanks. It is always good to hear from the original author :). Do I need to create an additional class to store this info or does allauth take care of that automatically? – Shreyas Sep 07 '12 at 20:10
  • 13
    That actually depends on the information you are asking. In any case, this is all beyond allauth scope. If you ask for first/last name as in the example above, then you do not need an extra model and can place things directly into the User model. If you would ask for the user's birthdate, his favorite color or whatever, then you need to setup your own profile model for this. Please have a look here on how to do this: https://docs.djangoproject.com/en/dev/topics/auth/#storing-additional-information-about-users – pennersr Sep 09 '12 at 20:36
  • 6
    That's exactly what I was looking for - an additional field like favorite color. In case I am interested in say, the favorite color, I believe I should create a new UserProfile class and then use the User as a one to one field and the favorite color as the additional field. In that case, can I still use a type of SignUpForm you have declared (with favorite color) above and hook up the ACCOUNT_SIGNUP_FORM_CLASS to it or do I need to create the form and handle saving of data in my own code? – Shreyas Sep 10 '12 at 01:03
  • 4
    Sure, the ACCOUNT_SIGNUP_FORM_CLASS mechanism can still be used. You just have to make sure that the save() method is properly implemented such that the favorite color is stored in whatever model you want. – pennersr Sep 11 '12 at 21:15
  • 5
    @pennersr - How could I hoop up this `ACCOUNT_SIGNUP_FORM_CLASS` after the very first social signin to collect and save the custom user model fields? Also the usage of custom user model by `AUTH_USER_MODEL` changes from git:https://github.com/pennersr/django-allauth are not uploaded in pypi. – Babu Nov 23 '12 at 18:46
  • 1
    +1 to @Babu questions :| I'm a bit confused about the workflow – VAShhh Feb 18 '13 at 09:42
  • @VAShhh - Yeah, it'll be at first time. But all-auth is changing and becoming strong progressively. Check out the recent docs and pypi upload. In my concern, it just works right out of the box. :) I suggest you to dig through the code to reveal and it'll be interesting though. I learnt a bunch of django stuffs by that way. – Babu Feb 18 '13 at 13:51
  • 3
    Now it works as expected, the problem was due to the `SOCIALACCOUNT_AUTO_SIGNUP` parameter that has to be set to `False` in order to be sure that the custom form is correctly called after the first social signin ;) – VAShhh Feb 19 '13 at 14:11
  • Let me just understand a little bit. In my case to store the user favorite color. 1) I create a new class/model 2) I point this model on settings with: ACCOUNT_SIGNUP_FORM_CLASS 3) I use one_to_one on this model, pointing to the right user. Did I understand wrong? – Max Ferreira Feb 10 '14 at 21:09
  • Is there also a variable `ACCOUNT_LOGIN_FORM_CLASS`? I'm also wanting to customize the login form – wnajar Sep 01 '14 at 03:20
  • @pennersr Does this also work with latest django allauth i.e. 0.19.1? Somehow the signup method is not getting called. – Divick Mar 23 '15 at 09:37
  • My bad, I was using the following setting which is why the signup was not getting called:
    'signup': 'api.forms.UserSignupForm'
    Instead using the setting: ACCOUNT_SIGNUP_FORM_CLASS seems to work fine. Though I see that signup is called after the form is already saved. It is ok to save additional data after the user has been persisted to DB but ideally it would be better to set up additional data before the user is saved to DB to avoid extra overhead of saving user object twice to DB.
    – Divick Mar 23 '15 at 10:05
  • If I understand correctly then we need to write custom AccountAdapter and override the save_user method and pass commit=False in super.save_user so that the user is not saved to DB and then we can save additional fields to model. – Divick Mar 23 '15 at 10:47
  • I'm using ACCOUNT_SIGNUP_FORM_CLASS = 'app.accounts.forms.UserSignupForm' But I get the error ImproperlyConfigured: Module "app.accounts.forms" does not define a "UserSignupForm" class All paths are correct. What am I missing? – dyve Apr 07 '15 at 11:14
  • 1
    My issue was due to circular import, just found out. – dyve Apr 07 '15 at 11:18
  • 1
    Is there any way I can override the existing signup form or the one which comes after social login and ask for username? I want to add one more field in both and like to change the name of the 'Username' field to something else. – A J Jul 15 '15 at 09:42
  • 1
    This solution leads to AttributeError: 'SignupForm' object has no attribute 'save', did anybody experience it and know how to get rid of this exception? – TeoTN Dec 05 '15 at 15:37
  • How can change the response of signup? – Moe Far Jan 24 '16 at 07:14
  • We are simply pointing to the form. How will it be rendered, how can we customize its html? – Darshan Chaudhary Apr 10 '16 at 20:23
  • Thank you @pennersr. How can I handle Integrity in this form? I have an avatar upload in the signup method and I need to rollback if something is wrong and also get info to show to the user. Thanks in advance! – azuax Jul 19 '16 at 13:42
  • @pennersr how can we tie up clean and signup function? Suppose if first_name and last_name can only be of 10 character then where should we write this validation logic when using signup() – pythonBeginner Mar 23 '17 at 01:39
  • How do I view Profile here {{user.profile.first_name }} or{{user.first_name}} does not work either. – ytsejam Sep 03 '17 at 20:41
24

Using the solution suggested by pennersr I was getting a DeprecationWarning:

DeprecationWarning: The custom signup form must offer a def signup(self, request, user) method DeprecationWarning)

This is because as of version 0.15 the save method has been deprecated in favour of a def signup(request, user) method.

So to solve this, the code of the example should be like this:

class SignupForm(forms.Form):
    first_name = forms.CharField(max_length=30, label='Voornaam')
    last_name = forms.CharField(max_length=30, label='Achternaam')

    def signup(self, request, user):
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.save()
ferrangb
  • 2,012
  • 2
  • 20
  • 34
20

Here's what worked for me combining a few of the other answers (none of them are 100% complete and DRY).

In yourapp/forms.py:

from django.contrib.auth import get_user_model
from django import forms

class SignupForm(forms.ModelForm):
    class Meta:
        model = get_user_model()
        fields = ['first_name', 'last_name']

    def signup(self, request, user):
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.save()

And in settings.py:

ACCOUNT_SIGNUP_FORM_CLASS = 'yourapp.forms.SignupForm'

This way it uses the model forms so that it's DRY, and uses the new def signup. I tried putting 'myproject.myapp.forms.SignupForm' but that resulted in a error somehow.

Howardwlo
  • 201
  • 2
  • 2
7

@Shreyas: The below solution may not be the cleanest, but it works. Please let me know if you have any suggestions to clean it up any further.

To add information that does not belong to the default user profile, first create a model in yourapp/models.py. Read the general django docs to learn more about it, but basicly:

from django.db import models

class UserProfile(models.Model):
    user = models.OneToOneField(User, related_name='profile')
    organisation = models.CharField(organisation, max_length=100, blank=True)

Then create a form in yourapp/forms.py:

from django import forms

class SignupForm(forms.Form):
    first_name = forms.CharField(max_length=30, label='Voornaam')
    last_name = forms.CharField(max_length=30, label='Achternaam')
    organisation = forms.CharField(max_length=20, label='organisation')

    def signup(self, request, user):
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        # Replace 'profile' below with the related_name on the OneToOneField linking back to the User model
        up = user.profile
        up.organisation = self.cleaned_data['organisation']
        user.save()
        up.save()
mark
  • 181
  • 2
  • 2
  • This is exactly what I ended up using for a Django 2.0 app that's running Wagtail CMS. Worked for regular sign up, but less so with Social Auth it seems? – Kalob Taulien Jul 06 '18 at 16:58
  • 1
    How would I add this extra field to the admin page for the user in Wagtail? – Joshua Sep 16 '19 at 19:18
5

In your users/forms.py you put:

from django.contrib.auth import get_user_model
class SignupForm(forms.ModelForm):
    class Meta:
        model = get_user_model()
        fields = ['first_name', 'last_name']
    def save(self, user):
        user.save()

In settings.py you put:

ACCOUNT_SIGNUP_FORM_CLASS = 'users.forms.SignupForm'

In this way you don't break DRY principle by multiplicity User models fields definition.

Adam Dobrawy
  • 1,145
  • 13
  • 14
4

I've tried many different tutorials and all of them is missing something, repeating unnecessary code or doing weird things, bellow follows my solution that joins all the options that I've found, it's working, I have already put it in production BUT it still not convincing me because I would expect to receive first_name and last_name inside the functions that I attached to Users create to avoid creating a profile inside the form but I couldn't, by the away I think it will help you.

Models.py

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    nationality = models.CharField(max_length=2, choices=COUNTRIES)
    gender = models.CharField(max_length=1, choices=GENDERS)

def __str__(self):
    return self.user.first_name


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

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

Forms.py

class SignupForm(forms.ModelForm):
    first_name = forms.CharField(max_length=100)
    last_name = forms.CharField(max_length=100)

    class Meta:
        model = Profile
        fields = ('first_name', 'last_name', 'nationality', 'gender')

    def signup(self, request, user):
        # Save your user
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.save()

        user.profile.nationality = self.cleaned_data['nationality']
        user.profile.gender = self.cleaned_data['gender']
        user.profile.save()

Settings.py

ACCOUNT_SIGNUP_FORM_CLASS = 'apps.profile.forms.SignupForm'
Gregory
  • 6,514
  • 4
  • 28
  • 26
  • Ironically, this is missing a few things too. The Profile fields have no defaults, nor do they allow null, so your `create_user_profile` signal fails by design. Secondly, you can reduce this to one signal, based on `created`, especially when talking DRY. And third, you effect a Profile save by calling user.save() in your view, then saving the profile again with actual data. –  Aug 30 '17 at 14:35
  • @Melvyn Shouldn't it be `fields = [...]` with **square braces** instead of `fields = (...)` parenthesis ? – Ahtisham Jan 08 '18 at 14:39
  • It can, but does not have to be. It is only used read-only to check if the field on the Model should be part of the form. So it can be a list, tuple or set or any derivative thereof. Since tuples are not mutable, it makes more sense to use tuples and prevent accidental mutations. From a performance perspective, these collections are in practice too small to have any impact. Once the collection gets too long, it may make sense to switch to `exclude` instead. –  Jan 08 '18 at 14:59
0
#models.py

from django.conf import settings

class UserProfile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    image = models.ImageField(default='users/default.png', upload_to='users')
    fields = models.ForeignKey('Field' ,null=True ,on_delete=models.SET_NULL)
    category = models.ForeignKey('Category' ,null=True ,on_delete=models.SET_NULL)
    description = models.TextField()
    interests = models.ManyToManyField('Interests')

    ...

   def save(self, *args, **kwargs):
       super().save(*args, **kwargs)

...

def userprofile_receiver(sender, instance, created, *args, **kwargs):
    if created:
        userprofile = UserProfile.objects.create(user=instance)
    else:
        instance.userprofile.save()

post_save.connect(userprofile_receiver, sender=settings.AUTH_USER_MODEL)



 #forms.py

 class SignupForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(SignupForm, self).__init__(*args, **kwargs)
        self.fields['first_name'].widget = forms.TextInput(attrs={'placeholder': 'Enter first name'})
        self.fields['last_name'].widget = forms.TextInput(attrs={'placeholder': 'Enter last name'})

    first_name = forms.CharField(max_length=100)
    last_name = forms.CharField(max_length=100)

    interests  = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, help_text="Choose your interests", queryset=Interests.objects.all())

    image = forms.ImageField(help_text="Upload profile image ")
    fields = forms.ChoiceField(help_text="Choose your fields ")
    category = forms.ChoiceField(help_text="Choose your category")

    class Meta:
        model = UserProfile
        fields = ('first_name', 'last_name',  'name', 'image', 'fields', 'category', 'description', 'phone', 'facebook', 'twitter', 'skype', 'site', 'address', 'interests' ,'biography')
        widgets = {
             ...
            'description': forms.TextInput(attrs={'placeholder': 'Your description'}),
            'address': forms.TextInput(attrs={'placeholder': 'Enter address'}),
            'biography': forms.TextInput(attrs={'placeholder': 'Enter biography'}),
            ....
    }
    def signup(self, request, user):
        # Save your user
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.save()

        user.userprofile.image = self.cleaned_data.get('image')
        user.userprofile.fields = self.cleaned_data['fields']
        user.userprofile.category = self.cleaned_data['category']
        user.userprofile.description = self.cleaned_data['description']
        interests = self.cleaned_data['interests']
        user.userprofile.interests.set(interests)
        user.userprofile.save()


# settings.py or base.py

ACCOUNT_SIGNUP_FORM_CLASS = 'nameApp.forms.SignupForm'

That is it. (:

Milovan Tomašević
  • 6,823
  • 1
  • 50
  • 42
-10

Create a Profile Model with user as OneToOneField

class Profile(models.Model):
    user = models.OneToOneField(User, verbose_name=_('user'), related_name='profiles')
    first_name=models.CharField(_("First Name"), max_length=150)
    last_name=models.CharField(_("Last Name"), max_length=150)
    mugshot = ImageField(_('mugshot'), upload_to = upload_to, blank=True)
    phone= models.CharField(_("Phone Number"), max_length=100)
    security_question = models.ForeignKey(SecurityQuestion, related_name='security_question')
    answer=models.CharField(_("Answer"), max_length=200)
    recovery_number= models.CharField(_("Recovery Mobile Number"), max_length=100)
    city=models.ForeignKey(City,related_name='city', blank=True, null=True, help_text=_('Select your City'))
    location=models.ForeignKey(Country,related_name='location', blank=True, null=True, help_text=_('Select your Location'))
Jubin Thomas
  • 1,003
  • 2
  • 12
  • 24
  • 3
    Thanks, but I don't think that this is all that is needed. My question specifically refers to the allauth app. – Shreyas Sep 06 '12 at 15:52