0

I'm trying to create a multi form signup with django allauth. (I originally tried django wizard, but opted out of that as sign up isn't necessarily a linear path, depending on earlier form data filled out by the user). What's the best way to get django allauth to work with multi page signup?

I thought that using a series of form views with the first creating the user and login them in:

@require_http_methods(['GET', 'HEAD', 'POST'])
def profile_view(request, price_id):
    form = ProfileForm()
    if request.method == 'POST':
        form = ProfileForm(request.POST)
        if form.is_valid():
            form.save()
            user = authenticate(request, username=form.cleaned_data['email'],
                                password=form.cleaned_data['password1'])
            login(request, user)
            return redirect('users:supply_address')
    return render(request, 'pages/signup.html', {'form': form})

followed by a series of similar views which require login and then ending with a view that uses the complete_signup method from django-allauth.

@login_required
@require_http_methods(['GET', 'HEAD', 'POST'])
def direct_debit_view(request):
    form = DirectDebitForm()
    if request.method == 'POST':
        form = DirectDebitForm(data=request.POST)
        if form.is_valid():
            request.session.update(form.cleaned_data)
            complete_signup(request, user, settings.ACCOUNT_EMAIL_VERIFICATION,settings.LOGIN_REDIRECT_URL)
            return redirect(settings.LOGIN_REDIRECT_URL)
    return render(request, 'pages/signup_postcode.html', {'form': form})

And overridding the url used for login for django-allauth to point to the first signup view. However I'm not sure if this is the best approach?

 url(r'^account/signup/$', views.profile_view, name='profile'),
Yunti
  • 6,761
  • 12
  • 61
  • 106
  • You need a different approach. The first form already creates a valid user that can log in. So, signup is done for all intents and purposes. The rest is completing the profile. This is OK and how most signups work, however if you don't want a user account created before everything is complete, you need to use sessions. The final form needs to have the union of the fields and get populated partially by what's in the session. –  Sep 11 '17 at 16:01
  • I started going down the session route but that got into more difficulty with types that weren't supported in json encoding. I'm ok with the user account being created first of all and then adding more data to it afterwards (although not perfect situation), I just don't want to have an email sent out to the user before all of the signup process is finished. If a user doesn't finish signup and complete the data, then I'll be able to see the user account with the incomplete details. – Yunti Sep 11 '17 at 18:37
  • I'm pretty much in the same boat, except that I have two user types with totally different profile models. I see a way though, through the use of "next". You can override the redirect url if you provide next in request.POST or request.GET. Passing it as a form field is the road I'm taking now. If no one chimes in, I'll post some code when I have it all working. –  Sep 11 '17 at 20:58

1 Answers1

0

So the basic idea is given here. In my case, I need one field always checked, since it's acceptance of terms and conditions. It's too annoying to deal with custom user model at this stage of the project, so I made it a requirement and saved in the profile [1].

The profile model (and this is important for all profile models to work pleasantly) has all fields with null=True or a default. This allows us to programmatically create them with minimal or even no available information.

class BaseProfile(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL, related_name='profile',
    )
    accepted_terms = models.BooleanField(
        default=False, verbose_name=_('terms accepted'),
    )

Then we need a form for the extra info. Ignore the account_type bits, it's because we have different account types, with different profile models tacking onto the base profile:

from allauth.utils import get_request_param
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _

from kernel.models import create_profile_for_user

class RequireTrueField(forms.BooleanField):
    default_error_messages = {
        'require_true': _('This field must be true')
    }
    widget = forms.CheckboxInput

    def validate(self, value):
        if not value:
            raise ValidationError(self.error_messages['require_true'],
                                  code='require_true')

class AccountSignupForm(forms.Form):
    error_css_class = 'has-error'
    accept_terms = RequireTrueField(
        initial=False, error_messages={
            'require_true': _('You must accept the terms and conditions')
        }, label=_('I agree to the terms and conditions')
    )

    def signup(self, request, user):
        account_type = get_request_param(request, 'account_type', '')
        accepted = self.cleaned_data['accept_terms']
        try:
            profile, created = create_profile_for_user(
                user, account_type, accepted_terms=accepted,
            )
            # XXX: This should always generate created as True. Verify?
        except TypeError:
            # XXX: spam someone
            pass

In the template, we put a hidden field with "next":

<input type="hidden" name="next" value="{% url 'kernel:profile_signup' %}"/>

Which allauth will use as success_url. Note that if it's not set, the user will be created, but the form will not redirect.

So now we have a user with a profile model, minimal data and can ask the next batch of questions. For each stage, implement a BooleanField on the profile model. Then guard views that require an active account with all booleans ticked, using UserPassesTestMixin.

[1] The idea of saving it in the first place, is because if terms and conditions change, we can change the value of the field and deny login till it's ticked again.