Building a User Registration System with Django Forms and AJAX

Software development typically follows a structured process beginning with requirements gathering, where stakeholders define the desired features. This leads to architectural decisions involving framework selection, database design, and task breakdown. Development teams then work in parallel on assigned modules, followed by quality assurance testing and final deployment.

Database Schema Design

When designing a blog platform database, we identify several core entities and their relationships:

User Information Table

Extends Django's built-in authentication system with additional fields:

  • Mobile number
  • Profile avatar (file upload)
  • Registration timestamp
  • Foreign key relationship to personal blog site

Personal Blog Site Table

Stores user-specific blog configurations:

  • Site name
  • Site title
  • Theme styling (CSS file path)

Article Tag Table

  • Tag name
  • Foreign key to blog site (many-to-one)

Article Category Table

  • Category name
  • Foreign key to blog site (many-to-one)

Article Table

Contains the main content along with optimization fields:

  • Title, summary, content body, publication date
  • Counter fields: upvote count, downvote count, comment count
  • Foreign key to blog site (many-to-one)
  • Foreign key to category (many-to-one)
  • Many-to-many relationship to tags (semi-automatic through table)

Article Rating Table

Tracks user reactions to articles:

  • User foreign key (many-to-one)
  • Article foreign key (many-to-one)
  • is_upvote boolean field

Comment Table

Supports nested comments through self-referential relationship:

  • User foreign key
  • Article foreign key
  • Content text field
  • Creation timestamp
  • Parent comment reference (self-referential foreign key for threaded discussions)

Model definitions:


# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'blog_platform',
        'USER': 'root',
        'PASSWORD': 'your_password',
        'HOST': 'localhost',
        'PORT': 3306,
    }
}

AUTH_USER_MODEL = 'core.CustomUser'

# models.py
from django.db import models
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    avatar = models.ImageField(upload_to='avatars/', default='static/default_avatar.png')
    blog = models.OneToOneField('Blog', on_delete=models.CASCADE, null=True)
    
class Blog(models.Model):
    title = models.CharField(max_length=100)
    site_name = models.CharField(max_length=64)
    theme_stylesheet = models.CharField(max_length=255)
    
class Tag(models.Model):
    name = models.CharField(max_length=32)
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    
class Category(models.Model):
    name = models.CharField(max_length=32)
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    
class Article(models.Model):
    title = models.CharField(max_length=128)
    summary = models.TextField()
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    upvote_count = models.IntegerField(default=0)
    downvote_count = models.IntegerField(default=0)
    comment_count = models.IntegerField(default=0)
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
    tags = models.ManyToManyField(Tag, through='ArticleTag', through_fields=('article', 'tag'))
    
class ArticleTag(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    
class ArticleVote(models.Model):
    user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    is_upvote = models.BooleanField()
    
class Comment(models.Model):
    user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    text = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)

Building the Registration Form

Create a dedicated forms module to handle validation logic:


# forms/registration_form.py
from django import forms
from core.models import CustomUser

class RegistrationForm(forms.Form):
    username = forms.CharField(
        max_length=8,
        min_length=3,
        label='Username',
        error_messages={
            'max_length': 'Username cannot exceed 8 characters',
            'min_length': 'Username must be at least 3 characters',
            'required': 'Username is required',
        },
        widget=forms.TextInput(attrs={'class': 'form-control'})
    )
    
    password = forms.CharField(
        max_length=32,
        min_length=6,
        label='Password',
        widget=forms.PasswordInput(attrs={'class': 'form-control'}),
        error_messages={
            'required': 'Password is required',
            'min_length': 'Password must be at least 6 characters'
        }
    )
    
    confirm_password = forms.CharField(
        label='Confirm Password',
        widget=forms.PasswordInput(attrs={'class': 'form-control'}),
        error_messages={'required': 'Please confirm your password'}
    )
    
    email = forms.EmailField(
        label='Email Address',
        error_messages={
            'invalid': 'Please enter a valid email address',
            'required': 'Email is required',
        },
        widget=forms.EmailInput(attrs={'class': 'form-control'})
    )
    
    def clean_username(self):
        input_username = self.cleaned_data.get('username')
        if CustomUser.objects.filter(username=input_username).exists():
            self.add_error('username', 'This username is already taken')
        return input_username
    
    def clean(self):
        cleaned = super().clean()
        pwd = cleaned.get('password')
        pwd_confirm = cleaned.get('confirm_password')
        if pwd and pwd_confirm and pwd != pwd_confirm:
            self.add_error('confirm_password', 'Passwords do not match')
        return cleaned

The view function renders the form:


# views.py
from django.shortcuts import render
from forms.registration_form import RegistrationForm

def show_registration_page(request):
    reg_form = RegistrationForm()
    return render(request, 'account/register.html', {'form': reg_form})

Template implementation with avatar preview:


<!-- register.html -->
<form id="registration_form">
    {% csrf_token %}
    {% for field in form %}
    <div class="form-group">
        <label for="{{ field.auto_id }}">{{ field.label }}</label>
        {{ field }}
        <span class="error-message text-danger"></span>
    </div>
    {% endfor %}
    <div class="form-group">
        <label for="avatar_input">
            Profile Avatar
            <img src="/static/img/default_avatar.png" id="avatar_preview" 
                 width="100" style="margin-left: 10px;">
        </label>
        <input type="file" name="avatar" id="avatar_input" style="display: none">
    </div>
    <button type="button" class="btn btn-primary" id="register_btn">Create Account</button>
</form>

<script>
$('#avatar_input').change(function() {
    var reader = new FileReader();
    var selectedFile = this.files[0];
    reader.readAsDataURL(selectedFile);
    reader.onload = function(e) {
        $('#avatar_preview').attr('src', e.target.result);
    };
});
</script>

Implementing Registration with AJAX

The view handler processes form submission and file upload:


# views.py
from django.http import JsonResponse
from django.contrib.auth import get_user_model

def process_registration(request):
    reg_form = RegistrationForm()
    
    if request.method == 'POST':
        response_data = {'status': 'success', 'message': '', 'redirect_url': ''}
        reg_form = RegistrationForm(request.POST)
        
        if reg_form.is_valid():
            valid_data = reg_form.cleaned_data
            valid_data.pop('confirm_password')
            
            uploaded_avatar = request.FILES.get('avatar')
            if uploaded_avatar:
                valid_data['avatar'] = uploaded_avatar
                
            User = get_user_model()
            User.objects.create_user(**valid_data)
            
            response_data['message'] = 'Registration successful'
            response_data['redirect_url'] = '/login/'
        else:
            response_data['status'] = 'error'
            response_data['errors'] = reg_form.errors
            
        return JsonResponse(response_data)
        
    return render(request, 'account/register.html', {'form': reg_form})

Client-side AJAX handling:


<script>
$('#register_btn').click(function() {
    var formData = new FormData();
    
    // Serialize regular form fields
    $.each($('#registration_form').serializeArray(), function(idx, item) {
        formData.append(item.name, item.value);
    });
    
    // Append file data
    var avatarFile = $('#avatar_input')[0].files[0];
    if (avatarFile) {
        formData.append('avatar', avatarFile);
    }
    
    $.ajax({
        url: '',
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        success: function(response) {
            if (response.status === 'success') {
                window.location.href = response.redirect_url;
            } else {
                $.each(response.errors, function(fieldName, errorMessages) {
                    var errorElement = '#id_' + fieldName;
                    $(errorElement).next('.error-message').text(errorMessages[0])
                                   .parent().addClass('has-error');
                });
            }
        }
    });
});

// Clear error styling on focus
$('input').focus(function() {
    $(this).next('.error-message').text('')
           .parent().removeClass('has-error');
});
</script>

Key Implementation Notes

For larger projects, organize forms into a dedicated package with separate modules for different functionalities. The FileReader API provides asynchronous file reading capabilities—use the onload callback to handle the result after reading completes. Image elements can accept file paths, base64 encoded data, or backend URLs that serve binary content.

Posted on Sun, 19 Jul 2026 17:14:04 +0000 by slicer123