Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple user type sign up with django-allauth

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?
like image 411
mrnfrancesco Avatar asked Jun 12 '17 17:06

mrnfrancesco


2 Answers

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
like image 185
Alex Gustafson Avatar answered Nov 18 '22 06:11

Alex Gustafson


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!

like image 18
mrnfrancesco Avatar answered Nov 18 '22 05:11

mrnfrancesco