14

EDIT

Please, do not waste your time reading the question... it is the wrong approach!

Look at my own answer for a step-by-step guide (with explanation) of the right solution

TL;DR

How could I implement sign up for private and company users using django-allauth?

The approach I'm following (is it correct?)

I have the following models:

class PrivateUser(models.Model):
    """Models a private user account"""
    user = models.OneToOneField(User, on_delete=models.CASCADE)


class CompanyUser(models.Model):
    """Models the company's contact person user account"""
    user = models.OneToOneField(User, on_delete=models.CASCADE)


class Company(models.Model):
    """Models the company attributes"""
    contact_person = models.OneToOneField(User, related_name='company')
    name = models.CharField(max_length=50, null=False, blank=False)
    vat_no = models.CharField(
        # some config and validators
    )
    # ... other non-relevant fields

Now, I have to distinguish between the two users PrivateUser and CompanyUser during the sign up process with django-allauth having just one sign up form as specified in the official django-allauth documentation:

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 def signup(self, request, user) method, where user represents the newly signed up user.

So, to create a unique form I created an abstract model class with all the fields from the PrivateUser and the CompanyUser plus one (note the user_type field):

class AbstractComprehensiveUser(models.Model):
    """
    Little hackish model class needed to handle one single sign up
    form for multiple users
    """

    USER_TYPE_CHOICES = (
        ('private', 'Private'),
        ('company', 'Company'),
    )

    user_type = models.CharField(
        max_length=10,
        blank=False,
        choices=USER_TYPE_CHOICES
    )

    # Common fields for either private and company users
    first_name = models.CharField(max_length=30, blank=False)
    last_name = models.CharField(max_length=30, blank=False)

    # Company specific fields
    company_name = models.CharField(max_length=50, null=True, blank=True)
    company_vat_no = models.CharField(
        # some config and validators
        null=True,
        blank = True
    )
    # other non-relevant fields

    class Meta:
        abstract = True

N.B: all the non-common fields have in this class the attributes null=True and blank=True.

Then I created my custom SignupForm as follow:

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

    class Meta:
        model = AbstractComprehensiveUser
        fields = (
            # Field to differentiate from private and company
            # user sign up
            'user_type',
            # Common fields for either private and company users
            'first_name', 'last_name',
            # Company specifc fields
            'company_name', 'company_vat_no', # etc etc
        )

The idea, now, is to use a template with two forms:

  • the one with hidden user_type='private' and just the first_name and last_name fields
  • the one with hidden user_type='company' and the fields from Company model

Then, in the SignupForm I will receive the user_type field and I could set the proper form, for example:

class PrivateUserSignupForm(forms.ModelForm):
    first_name = forms.CharField(max_length=30)
    last_name = forms.CharField(max_length=30)

    class Meta:
        model = PrivateUser
        fields = ('first_name', 'last_name')

The problem is that when I retrieve data in the SignupForm.signup() method, the User model is already written in the database.

I would like to do not save it, but just:

  • validating it
  • receive data in the signup method to populate the correct form (PrivateUserSignupForm or CompanyUserSignupForm)
  • validate the form
    • in case of no errors save the user and the other models
    • in case of error do not save nothing and warn the user about the error(s)

The question are...

  • is this approach correct? There's some other way to accomplish this without these compilcation?
  • if this approach is correct, how could I handle the workflow described just above?
mrnfrancesco
  • 1,017
  • 8
  • 26

2 Answers2

19

TL;DR

All the messy stuff I wrote above are junk!

The (final) right solution

In settings.py remove ACCOUNT_SIGNUP_FORM_CLASS, we won't use it.

Suppose to have the following models:

class PrivateUser(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)

class CompanyUser(models.Model):
    contact_person = models.OneToOneField(User, on_delete=models.CASCADE)
    company_name = models.CharField(max_length=50, null=False, blank=False)

Now, what we want is to let our app signup the PrivateUser and the CompanyUser with different forms.

To accomplish that we'll extends the django-allauth's SignupForm and SignupView.

In forms.py:

from myapp.models import CompanyUser

class CompanySignupForm(SignupForm):
    # declare here all the extra fields in CompanyUser model WITHOUT
    # the OneToOneField to User
    # (N.B: do NOT try to declare Meta class with model=CompanyUser,
    # it won't work!)
    company_name = forms.CharField(max_length=50, required=True, strip=True)

    # Override the save method to save the extra fields
    # (otherwise the form will save the User instance only)
    def save(self, request):
        # Save the User instance and get a reference to it
        user = super(CompanySignupForm, self).save(request)
        # Create an instance of your model with the extra fields
        # then save it.
        # (N.B: the are already cleaned, but if you want to do some
        # extra cleaning just override the clean method as usual)
        company_user = CompanyUser(
            contact_person=user,
            company_name=self.cleaned_data.get('company_name')
        )
        company_user.save()

        # Remember to return the User instance (not your custom user,
        # the Django one), otherwise you will get an error when the
        # complete_signup method will try to look at it.
        return company_user.contact_person

Now, we have CompanyUser model and CompanySignupForm form. Let's create a CompanyUserSignupView view in views.py with the following code:

class CompanyUserSignupView(SignupView):
    # The referenced HTML content can be copied from the signup.html
    # in the django-allauth template folder
    template_name = 'account/signup_company.html'
    # the previously created form class
    form_class = CompanySignupForm

    # the view is created just a few lines below
    # N.B: use the same name or it will blow up
    view_name = 'company_signup'

    # I don't use them, but you could override them
    # (N.B: the following values are the default)
    # success_url = None
    # redirect_field_name = 'next'

# Create the view (we will reference to it in the url patterns)
company_signup = CompanyUserRegistrationView.as_view()

Last step, the urls.py:

urlpatterns = [
    # ...
    url(
        r'^accounts/signup/company/$',
        views.company_signup,
        name='signup-company'
    ),
]

Now, just use your browser to go to http://localhost:8000/accounts/signup/company (or the proper url pattern based on your configuration).

You will find the extra fields and you can signup a company user.

Now repeat all the previous steps to create a PrivateSignupForm form, a PrivateUserSignupView view and add the proper url pattern to let users signup as privates.

LAST WARNING

The django-allauth default signup url will still works unless you override it with one of your url... and you should do that!

mrnfrancesco
  • 1,017
  • 8
  • 26
  • Thank you! But, how do you check if user is of type private or company? – user1518217 Jan 05 '18 at 10:11
  • You could create your own "User" class with a field named like "user_type" and two methods "isPrivate" and "isCompany" which return true or false if "user_type" field is indicating the user is a private or a company one. Now, when you are in the save method of the PrivateUserSignupForm (for istance), just after you get the user object, type "user.user_type = " (I suggest an Enum for that). – mrnfrancesco Jan 06 '18 at 19:33
  • How do you override the default url? – L C May 30 '20 at 03:00
  • 1
    It's been a while, but If I don't go wrong you could just add to your URLs patterns the regexp that match the signup default URL and point it to a custom view. If it is evaluated before the default one, it become the default. – mrnfrancesco May 31 '20 at 11:23
  • 1
    Ah thank you! Do you need to change the ACCOUNT_FORMS in settings.py? The documentation (https://django-allauth.readthedocs.io/en/latest/forms.html#signup-allauth-account-forms-signupform) seems to change the settings file, but since we have 2 forms here, I'm not even sure what to change the signup form settings to. – L C Jun 01 '20 at 10:26
  • The simplest (even if a bit hackish) way is to map the default signup URL to a 404 Not Found page. In this way only the custom signup forms will be available – mrnfrancesco Jun 02 '20 at 13:08
  • @LC have you figure it out ? – cristian Nov 04 '20 at 11:43
  • 3
    I think you meant to write `company_signup = CompanyUserSignupView.as_view()` instead of `company_signup = CompanyUserRegistrationView.as_view()` – cristian Nov 04 '20 at 11:45
  • 2
    This is not working solution for multiple forms !!! – cristian Nov 04 '20 at 11:59
  • For anyone who reads this answer in the future: I had to override the `get_form_class` method in the view for this to work, because the `SignupView` defines it so that it returns the form class you set in the `ACCOUNT_FORMS` setting, if you set it of course. – Samuel Ochoa May 24 '22 at 21:50
11

I had the same problem. I needed to use allauth for different user profile types. I extended the allauth SignupView and used it as a In my case I have a MemberProfile and PartnerProfile:

#profile models

class MemberProfile(models.Model):
  user = models.OneToOneField(
    settings.AUTH_USER_MODEL,
    on_delete=models.CASCADE,
  )


class PartnerProfile(models.Model):
  user = models.OneToOneField(
    settings.AUTH_USER_MODEL,
    on_delete=models.CASCADE,
  )

I want a separate signup page for each type of profile. Luckily the allauth SignupView stores the user on it's instance in the form_value() method. I extend the SignupView as ProfileView which expects a profile_class :

#mixin

from allauth.account.views import SignupView
from allauth.account.forms import SignupForm


class ProfileSignupView(SignupView):

  template_name = 'profiles/register.html'
  success_url = ''  # profile specific success url
  form_class = SignupForm
  profile_class = None  # profile class goes here

  def form_valid(self, form):
    response = super(ProfileSignupView, self).form_valid(form)
    profile = self.profile_class(user=self.user)
    profile.save()

    return response

then my views look like this:

#views

from .mixins import ProfileSignupView
from .models import PartnerProfile, MemberProfile

class MemberSignupView(ProfileSignupView):

   success_url = '/member/profile'
   profile_class = MemberProfile


class PartnerSignupView(ProfileSignupView):

    success_url = '/partner/profile'
    profile_class = PartnerProfile
Alex Gustafson
  • 198
  • 1
  • 8
  • May I ask why you also write the form_valid method (since mmfrancesco's response doesn't)? – L C Jun 01 '20 at 13:34
  • mmfranco is extending the SignupForm and overriding the save method. That's valid. I decided to extend the SignupView and override the form_valid method which calls the save method on the form as well. The difference is that I am creating the Profile after the complete_signup method has been called ( email notification has been sent ). mmfranco is creating the profile before that has happened. If your view is atomic, its the same. In my case I actually have three profiles, Partners, Members and Administrators. I found it easier to create one view for each, but use the identical form. – Alex Gustafson Jun 03 '20 at 11:04