Introduction

In the previous tutorial, you learned the basics of authentication in Django: signup, login, logout, and protecting pages. That is enough for simple applications, but real projects often need a more advanced registration system.

For example, you may want users to:

In this tutorial, you will build a more advanced user registration system in Django using:

By the end, you will have a stronger registration system ready for real-world projects.

What You Will Learn

By the end of this tutorial, you will understand:

Prerequisites

Before starting, you should already know:

1. Why Build an Advanced Registration System?

The default UserCreationForm is useful, but it is limited. It usually includes only:

In real projects, you often need more, such as:

A more advanced registration system gives a better user experience and helps collect the data your application really needs.

2. The Default User Model

Django already provides a built-in User model:

from django.contrib.auth.models import User

It already includes fields such as:

So in many cases, you do not need a custom user model yet. You can simply extend the default user with a profile model.

3. Create a Profile Model

A common approach is to create a separate profile linked to the user.

models.py

from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    phone = models.CharField(max_length=20, blank=True)
    city = models.CharField(max_length=100, blank=True)
    bio = models.TextField(blank=True)

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

Explanation

Run migrations after creating the model:

python manage.py makemigrations
python manage.py migrate

4. Create a Custom Signup Form

Now let us create a better signup form with extra fields.

forms.py

from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class AdvancedSignUpForm(UserCreationForm):
    email = forms.EmailField(required=True)
    first_name = forms.CharField(max_length=100, required=True)
    last_name = forms.CharField(max_length=100, required=True)
    phone = forms.CharField(max_length=20, required=False)
    city = forms.CharField(max_length=100, required=False)
    bio = forms.CharField(widget=forms.Textarea, required=False)

    class Meta:
        model = User
        fields = [
            'username',
            'first_name',
            'last_name',
            'email',
            'password1',
            'password2',
            'phone',
            'city',
            'bio',
        ]

Important Note

Fields like phone, city, and bio are not part of the User model. They belong to the Profile model. We include them in the form, but we will save them manually.

5. Add Validation for Email Uniqueness

By default, Django’s User model does not force unique emails unless you check manually.

Add a validation method

from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class AdvancedSignUpForm(UserCreationForm):
    email = forms.EmailField(required=True)
    first_name = forms.CharField(max_length=100, required=True)
    last_name = forms.CharField(max_length=100, required=True)
    phone = forms.CharField(max_length=20, required=False)
    city = forms.CharField(max_length=100, required=False)
    bio = forms.CharField(widget=forms.Textarea, required=False)

    class Meta:
        model = User
        fields = [
            'username',
            'first_name',
            'last_name',
            'email',
            'password1',
            'password2',
            'phone',
            'city',
            'bio',
        ]

    def clean_email(self):
        email = self.cleaned_data['email']
        if User.objects.filter(email=email).exists():
            raise forms.ValidationError('This email is already in use.')
        return email

This prevents two users from registering with the same email.

6. Save User and Profile Together

Now we need a signup view that:

views.py

from django.shortcuts import render, redirect
from django.contrib.auth import login
from .forms import AdvancedSignUpForm
from .models import Profile

def signup_view(request):
    if request.method == 'POST':
        form = AdvancedSignUpForm(request.POST)
        if form.is_valid():
            user = form.save(commit=False)
            user.email = form.cleaned_data['email']
            user.first_name = form.cleaned_data['first_name']
            user.last_name = form.cleaned_data['last_name']
            user.save()

            Profile.objects.create(
                user=user,
                phone=form.cleaned_data['phone'],
                city=form.cleaned_data['city'],
                bio=form.cleaned_data['bio'],
            )

            login(request, user)
            return redirect('dashboard')
    else:
        form = AdvancedSignUpForm()

    return render(request, 'accounts/signup.html', {'form': form})

7. Why Use commit=False?

This line is important:

user = form.save(commit=False)

It means:

This is useful when you want to add more data before saving.

8. Automatically Log In the User After Registration

After signup, many websites log the user in automatically.

We do that here:

from django.contrib.auth import login
login(request, user)

This improves user experience because they do not need to log in again immediately after registering.

9. Create the Signup Template

templates/accounts/signup.html

<h1>Create Your Account</h1>

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Register</button>
</form>

<p>
    Already have an account?
    <a href="{% url 'login' %}">Login here</a>
</p>

This version works, but it is very basic. Let us improve it.

10. Better Signup Template with Field Control

A more professional template gives you better control over layout and styling.

 

<h1>Create Your Account</h1>

<form method="post">
    {% csrf_token %}

    <p>
        <label for="{{ form.username.id_for_label }}">Username</label>
        {{ form.username }}
        {{ form.username.errors }}
    </p>

    <p>
        <label for="{{ form.first_name.id_for_label }}">First Name</label>
        {{ form.first_name }}
        {{ form.first_name.errors }}
    </p>

    <p>
        <label for="{{ form.last_name.id_for_label }}">Last Name</label>
        {{ form.last_name }}
        {{ form.last_name.errors }}
    </p>

    <p>
        <label for="{{ form.email.id_for_label }}">Email</label>
        {{ form.email }}
        {{ form.email.errors }}
    </p>

    <p>
        <label for="{{ form.phone.id_for_label }}">Phone</label>
        {{ form.phone }}
        {{ form.phone.errors }}
    </p>

    <p>
        <label for="{{ form.city.id_for_label }}">City</label>
        {{ form.city }}
        {{ form.city.errors }}
    </p>

    <p>
        <label for="{{ form.bio.id_for_label }}">Bio</label>
        {{ form.bio }}
        {{ form.bio.errors }}
    </p>

    <p>
        <label for="{{ form.password1.id_for_label }}">Password</label>
        {{ form.password1 }}
        {{ form.password1.errors }}
    </p>

    <p>
        <label for="{{ form.password2.id_for_label }}">Confirm Password</label>
        {{ form.password2 }}
        {{ form.password2.errors }}
    </p>

    <button type="submit">Register</button>
</form>

This makes it easier to style each field later with Bootstrap or custom CSS.

11. Add the URL

urls.py

from django.urls import path
from .views import signup_view

urlpatterns = [
    path('signup/', signup_view, name='signup'),
]

12. Create a Dashboard Page

After registration, we redirected the user to dashboard, so let us create it.

views.py

from django.contrib.auth.decorators import login_required
from django.shortcuts import render

@login_required
def dashboard(request):
    return render(request, 'accounts/dashboard.html')

templates/accounts/dashboard.html

<h1>Welcome {{ user.first_name|default:user.username }}</h1>
<p>You are now logged in.</p>

{% if user.profile %}
    <p>City: {{ user.profile.city }}</p>
    <p>Phone: {{ user.profile.phone }}</p>
{% endif %}

13. Accessing the Profile from the User

Because the relationship is one-to-one, you can access the profile like this:

user.profile

So in templates:

{{ user.profile.city }}
{{ user.profile.phone }}

This is very convenient.

14. Improve the Form with Widgets

You can make the form cleaner using widgets.

forms.py

from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class AdvancedSignUpForm(UserCreationForm):
    email = forms.EmailField(
        required=True,
        widget=forms.EmailInput(attrs={'placeholder': 'Enter your email'})
    )
    first_name = forms.CharField(
        max_length=100,
        required=True,
        widget=forms.TextInput(attrs={'placeholder': 'First name'})
    )
    last_name = forms.CharField(
        max_length=100,
        required=True,
        widget=forms.TextInput(attrs={'placeholder': 'Last name'})
    )
    phone = forms.CharField(
        max_length=20,
        required=False,
        widget=forms.TextInput(attrs={'placeholder': 'Phone number'})
    )
    city = forms.CharField(
        max_length=100,
        required=False,
        widget=forms.TextInput(attrs={'placeholder': 'City'})
    )
    bio = forms.CharField(
        required=False,
        widget=forms.Textarea(attrs={'placeholder': 'Short bio', 'rows': 4})
    )

    class Meta:
        model = User
        fields = [
            'username',
            'first_name',
            'last_name',
            'email',
            'password1',
            'password2',
            'phone',
            'city',
            'bio',
        ]

This improves the appearance and usability of the form.

15. Add Custom Validation for Username Rules

You can also validate usernames more strictly.

Example

def clean_username(self):
    username = self.cleaned_data['username']
    if len(username) < 4:
        raise forms.ValidationError('Username must be at least 4 characters long.')
    return username

Add this inside your form class.

16. Add Password Help Text or Simplify It

By default, UserCreationForm shows Django’s built-in password help rules.

If you want to keep them, that is fine.

If you want to customize them, you can do:

self.fields['password1'].help_text = 'Choose a strong password.'
self.fields['password2'].help_text = 'Enter the same password again.'

inside the form’s __init__() method.

Example:

class AdvancedSignUpForm(UserCreationForm):
    ...

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['password1'].help_text = 'Choose a strong password.'
        self.fields['password2'].help_text = 'Enter the same password again.'

17. Full Working Example

models.py

from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    phone = models.CharField(max_length=20, blank=True)
    city = models.CharField(max_length=100, blank=True)
    bio = models.TextField(blank=True)

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

forms.py

from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class AdvancedSignUpForm(UserCreationForm):
    email = forms.EmailField(required=True)
    first_name = forms.CharField(max_length=100, required=True)
    last_name = forms.CharField(max_length=100, required=True)
    phone = forms.CharField(max_length=20, required=False)
    city = forms.CharField(max_length=100, required=False)
    bio = forms.CharField(widget=forms.Textarea, required=False)

    class Meta:
        model = User
        fields = [
            'username',
            'first_name',
            'last_name',
            'email',
            'password1',
            'password2',
            'phone',
            'city',
            'bio',
        ]

    def clean_email(self):
        email = self.cleaned_data['email']
        if User.objects.filter(email=email).exists():
            raise forms.ValidationError('This email is already in use.')
        return email

views.py

from django.shortcuts import render, redirect
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from .forms import AdvancedSignUpForm
from .models import Profile

def signup_view(request):
    if request.method == 'POST':
        form = AdvancedSignUpForm(request.POST)
        if form.is_valid():
            user = form.save(commit=False)
            user.email = form.cleaned_data['email']
            user.first_name = form.cleaned_data['first_name']
            user.last_name = form.cleaned_data['last_name']
            user.save()

            Profile.objects.create(
                user=user,
                phone=form.cleaned_data['phone'],
                city=form.cleaned_data['city'],
                bio=form.cleaned_data['bio'],
            )

            login(request, user)
            return redirect('dashboard')
    else:
        form = AdvancedSignUpForm()

    return render(request, 'accounts/signup.html', {'form': form})

@login_required
def dashboard(request):
    return render(request, 'accounts/dashboard.html')

urls.py

from django.urls import path
from .views import signup_view, dashboard

urlpatterns = [
    path('signup/', signup_view, name='signup'),
    path('dashboard/', dashboard, name='dashboard'),
]

18. Common Beginner Mistakes

Adding profile fields directly to UserCreationForm without saving them

Extra fields like phone and city do not automatically save to the User model.

Forgetting to create the profile

If you collect profile data, you must save it into a Profile model.

Not validating unique email

Two accounts may end up using the same email.

Forgetting commit=False

You often need it when customizing user saving.

Redirecting to a protected page without logging the user in

If you redirect to the dashboard but do not log the user in, access may fail.

19. Best Practices

20. Optional Improvement: Create Profile Automatically with Signals

Instead of creating the profile inside the signup view, you can use signals.

Example

from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
from .models import Profile

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

This is useful, but for beginners, creating the profile directly in the signup view is easier to understand.

21. Optional Improvement: Add Profile Picture

Later, you can extend the profile with:

avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)

Then update the form and template to support image upload.

This is a very common real-world improvement.

22. Summary

In this tutorial, you learned how to build a more advanced user registration system in Django by:

This approach gives you a much more realistic registration system for real Django projects.

23. Mini Quiz

1. Which form is commonly extended for advanced signup?

A. ModelForm
B. LoginForm
C. UserCreationForm
D. AuthForm

2. Where should extra data like phone or city usually be stored?

A. In the URL
B. In a profile model
C. In settings.py
D. In middleware

3. What does commit=False do?

A. Deletes the form
B. Saves the form twice
C. Creates the object without saving to the database yet
D. Logs the user out

4. Which function can log the user in after signup?

A. authenticate()
B. login()
C. logout()
D. redirect()

5. Why validate email uniqueness?

A. To make the form longer
B. To improve template design
C. To prevent multiple accounts from using the same email
D. To disable login

24. What Comes Next?

The next ideal tutorial is:

Tutorial: Login Required Pages and Permissions
Subject: restricting access to views, user roles, permissions, and protected content.