Create a dating website in Python

How I Learned to Stop Worrying and Love the Code

Alright, listen up, gentlemen! So you want to build a dating website? Tired of swiping left on other people's apps and figured you'd build your own digital meat market? Smart move! This isn't just about coding—it's about creating the ultimate digital wingman.

335dae1535e6efa66a8c6f28450d09b6.png

Before we start, a warning: with great power comes great responsibility. Don't be "that guy" who builds a creepy stalker app. Be the guy who builds something actually useful that might, you know, help people connect. And maybe get you a date. No promises though—I'm a tutorial, not a magician.

Chapter 1: The Tools of the Trade

First, let's gear up. You wouldn't go to the gym in flip-flops, so don't build a dating app with notepad.

What You'll Need:

# requirements.txt - The VIP list for our code party
Django>=4.0          # The big boss framework
Pillow>=9.0          # For handling those profile pics
django-crispy-forms  # Making forms look less ugly
python-decouple      # Keeping secrets secret

Installation command (say it with me):

pip install -r requirements.txt

Why Django?

Django is like that reliable friend who brings structure to your chaotic life. It's got built-in admin panels, user authentication, and more features than a Swiss Army knife that's been to coding bootcamp.

Chapter 2: Project Setup - Birth of a Legend

Let's create our project. Think of this as building the foundation of your digital love shack.

# Create the project - choose a better name than I did
django-admin startproject digital_wingman
cd digital_wingman

# Create our main app
python manage.py startapp dating_core

# Create the profiles app (because profiles are kind of important)
python manage.py startapp profiles

Your project structure should look like this:

digital_wingman/
├── digital_wingman/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── dating_core/
│   ├── migrations/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── profiles/
│   └── ... (same structure)
└── manage.py

Chapter 3: Models - The Digital DNA of Love

Now for the fun part: defining what makes a profile. This is where we decide what matters in the digital dating world.

# profiles/models.py
from django.contrib.auth.models import User
from django.db import models
import os

def profile_pic_path(instance, filename):
    # This handles profile picture uploads
    # Because nobody trusts a profile without pics
    ext = filename.split('.')[-1]
    filename = f'profile_{instance.user.id}.{ext}'
    return os.path.join('profile_pics', filename)

class Profile(models.Model):
    # Basic info - the "about me" section
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)

    # The important stuff
    GENDER_CHOICES = [
        ('M', 'Male'),
        ('F', 'Female'),
        ('NB', 'Non-Binary'),
        ('O', 'Other'),
    ]

    gender = models.CharField(max_length=2, choices=GENDER_CHOICES)
    looking_for = models.CharField(max_length=2, choices=GENDER_CHOICES)

    # Age - because "old enough to be your dad" isn't a great pickup line
    birth_date = models.DateField(null=True, blank=True)

    # Location - for when you want to actually meet someone
    city = models.CharField(max_length=100, blank=True)
    country = models.CharField(max_length=100, blank=True)

    # Profile picture - the money shot
    profile_picture = models.ImageField(
        upload_to=profile_pic_path, 
        blank=True,
        default='profile_pics/default.jpg'
    )

    # Interests - because "breathing" isn't a personality
    interests = models.TextField(
        max_length=1000, 
        blank=True, 
        help_text="Separate interests with commas"
    )

    # Stats that matter
    height_cm = models.PositiveIntegerField(null=True, blank=True)

    # The dealbreakers
    smokes = models.BooleanField(default=False)
    drinks = models.BooleanField(default=False)

    # Timestamps
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f"{self.user.username}'s Profile"

    def age(self):
        """Calculate age from birth date"""
        import datetime
        if self.birth_date:
            today = datetime.date.today()
            return today.year - self.birth_date.year - (
                (today.month, today.day) < 
                (self.birth_date.month, self.birth_date.day)
            )
        return None

    def get_interests_list(self):
        """Convert interests string to list"""
        if self.interests:
            return [interest.strip() for interest in self.interests.split(',')]
        return []

But wait, there's more! We need a way to track who's checking out whom:

# dating_core/models.py
from django.db import models
from django.contrib.auth.models import User

class Swipe(models.Model):
    """Tracks left/right swipes - the heart of our operation"""
    swiper = models.ForeignKey(
        User, 
        on_delete=models.CASCADE, 
        related_name='swipes_made'
    )
    swiped_on = models.ForeignKey(
        User, 
        on_delete=models.CASCADE, 
        related_name='swipes_received'
    )

    SWIPE_CHOICES = [
        ('right', 'Right Swipe - Interested'),
        ('left', 'Left Swipe - Not Interested'),
        ('super', 'Super Swipe - Really Interested'),
    ]

    swipe_type = models.CharField(max_length=10, choices=SWIPE_CHOICES)
    swiped_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ['swiper', 'swiped_on']  # Prevent duplicate swipes

    def __str__(self):
        return f"{self.swiper} -> {self.swiped_on} ({self.swipe_type})"

class Match(models.Model):
    """When two people both swipe right - it's a match!"""
    user1 = models.ForeignKey(
        User, 
        on_delete=models.CASCADE, 
        related_name='matches_as_user1'
    )
    user2 = models.ForeignKey(
        User, 
        on_delete=models.CASCADE, 
        related_name='matches_as_user2'
    )
    matched_at = models.DateTimeField(auto_now_add=True)

    # Track if the match is still active
    is_active = models.BooleanField(default=True)

    class Meta:
        unique_together = ['user1', 'user2']

    def __str__(self):
        return f"Match: {self.user1} & {self.user2}"

Chapter 4: Making the Admin Panel Your Wingman

The Django admin is like having a super-powered assistant who handles all the boring stuff:

# profiles/admin.py
from django.contrib import admin
from .models import Profile

@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
    list_display = ['user', 'gender', 'looking_for', 'city', 'age']
    list_filter = ['gender', 'looking_for', 'smokes', 'drinks']
    search_fields = ['user__username', 'bio', 'city', 'interests']

    # Quick actions for the admin
    actions = ['mark_as_smoker', 'mark_as_non_smoker']

    def mark_as_smoker(self, request, queryset):
        queryset.update(smokes=True)
    mark_as_smoker.short_description = "Mark selected profiles as smokers"

    def mark_as_non_smoker(self, request, queryset):
        queryset.update(smokes=False)
    mark_as_non_smoker.short_description = "Mark selected profiles as non-smokers"

Chapter 5: Basic Views - Your First Digital Introduction

Let's create some basic views so we can actually see something working:

# dating_core/views.py
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from profiles.models import Profile
from .models import Swipe, Match

@login_required
def dashboard(request):
    """The main dashboard - where the magic happens"""
    user_profile = get_object_or_404(Profile, user=request.user)

    # Get potential matches (people you haven't swiped on yet)
    swiped_users = Swipe.objects.filter(
        swiper=request.user
    ).values_list('swiped_on', flat=True)

    # Exclude yourself and people you've already swiped on
    potential_matches = Profile.objects.exclude(
        user=request.user
    ).exclude(
        user__in=swiped_users
    )[:10]  # Show 10 at a time

    context = {
        'profile': user_profile,
        'potential_matches': potential_matches,
    }

    return render(request, 'dating_core/dashboard.html', context)

@login_required
def profile_detail(request, username):
    """View someone's profile in all its glory"""
    user = get_object_or_404(User, username=username)
    profile = get_object_or_404(Profile, user=user)

    context = {
        'viewed_profile': profile,
    }

    return render(request, 'profiles/profile_detail.html', context)

Chapter 6: URL Configuration - The Digital Roadmap

Now let's wire everything up so our app knows where to go:

# digital_wingman/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('dating_core.urls')),
    path('profiles/', include('profiles.urls')),
]

# This serves media files during development
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# dating_core/urls.py
from django.urls import path
from . import views

app_name = 'dating_core'

urlpatterns = [
    path('', views.dashboard, name='dashboard'),
]
# profiles/urls.py
from django.urls import path
from . import views

app_name = 'profiles'

urlpatterns = [
    path('<str:username>/', views.profile_detail, name='profile_detail'),
]

Chapter 7: Settings - Making It All Work

Finally, let's configure our settings so this thing actually runs:

# digital_wingman/settings.py
import os
from pathlib import Path
from decouple import config

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = config('SECRET_KEY')  # Keep this secret!

DEBUG = config('DEBUG', default=False, cast=bool)

ALLOWED_HOSTS = []

# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # Our apps
    'dating_core',
    'profiles',

    # Third party
    'crispy_forms',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'digital_wingman.urls'

# Templates
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# Database
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

# Media files (profile pictures)
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# Static files
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']

# Crispy forms
CRISPY_TEMPLATE_PACK = 'bootstrap4'

# Login URL
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'

Quick Start Commands:

# Create and run migrations
python manage.py makemigrations
python manage.py migrate

# Create a superuser (so you can access the admin)
python manage.py createsuperuser

# Run the development server
python manage.py runserver

What We've Built So Far:

Congratulations, you magnificent bastard! You've now got:

In the next part, we'll actually make this thing look good, add the swipe functionality, and create the matching logic. We'll also make sure it doesn't look like it was designed by a colorblind developer.

Remember: This is just the foundation. We haven't even gotten to the good stuff yet—like making sure people can actually, you know, match and talk to each other. But every great dating app starts with a solid foundation, and you've just poured the concrete!

Now go grab a drink (coffee, you animal), and get ready for Part 2 where we make this thing actually work!

How to Build a Dating Website: Part 2 - Making It Actually Work

Or: From Digital Wallflower to Smooth Operator

Alright, champ! You've built the foundation. Right now, your dating app has all the charm of a spreadsheet at a nightclub. Time to add some swagger! In this part, we're going from "meh" to "maybe I shouldn't show this to my mom."

Chapter 8: Templates - Making It Look Less Like Geocities

First, let's create some base templates so our app doesn't look like it's from 1998.

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Digital Wingman - Your Ultimate Wingman{% endblock %}</title>

    <!-- Bootstrap CSS - because we're developers, not designers -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .profile-card {
            transition: transform 0.2s;
            cursor: pointer;
        }
        .profile-card:hover {
            transform: translateY(-5px);
            box-shadow: 0 4px 15px rgba(0,0,0,0.1);
        }
        .swipe-buttons {
            position: fixed;
            bottom: 0;
            left: 0;
            right: 0;
            background: white;
            padding: 1rem;
            border-top: 1px solid #dee2e6;
        }
        .match-popup {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            z-index: 1000;
            display: none;
        }
    </style>
</head>
<body>
    <!-- Navigation - because getting lost is only romantic in movies -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="{% url 'dating_core:dashboard' %}">
                💘 Digital Wingman
            </a>

            {% if user.is_authenticated %}
            <div class="navbar-nav ms-auto">
                <a class="nav-link" href="{% url 'dating_core:dashboard' %}">Dashboard</a>
                <a class="nav-link" href="{% url 'profiles:profile_detail' user.username %}">My Profile</a>
                <a class="nav-link" href="{% url 'logout' %}">Logout</a>
            </div>
            {% endif %}
        </div>
    </nav>

    <!-- Main content -->
    <main class="container mt-4">
        {% if messages %}
            {% for message in messages %}
            <div class="alert alert-{{ message.tags }} alert-dismissible fade show">
                {{ message }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
            {% endfor %}
        {% endif %}

        {% block content %}
        <!-- This is where the magic happens -->
        {% endblock %}
    </main>

    <!-- Match popup - for when magic happens -->
    <div class="match-popup alert alert-success" id="matchPopup">
        <h4>🎉 It's a Match!</h4>
        <p>You and <span id="matchName">Someone</span> liked each other!</p>
        <button class="btn btn-primary" onclick="closeMatchPopup()">Send Message</button>
        <button class="btn btn-secondary" onclick="closeMatchPopup()">Keep Swiping</button>
    </div>

    <!-- Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>

    <!-- Our custom JS -->
    <script>
        function showMatchPopup(username) {
            document.getElementById('matchName').textContent = username;
            document.getElementById('matchPopup').style.display = 'block';
        }

        function closeMatchPopup() {
            document.getElementById('matchPopup').style.display = 'none';
        }

        // Swipe functionality
        function swipeRight(userId) {
            // We'll implement this properly later
            console.log('Swiped right on user:', userId);
        }

        function swipeLeft(userId) {
            // We'll implement this properly later  
            console.log('Swiped left on user:', userId);
        }
    </script>

    {% block extra_js %}{% endblock %}
</body>
</html>

Now let's create the dashboard template where users will spend most of their time:

<!-- templates/dating_core/dashboard.html -->
{% extends 'base.html' %}

{% block title %}Dashboard - Digital Wingman{% endblock %}

{% block content %}
<div class="row">
    <div class="col-md-3">
        <!-- Quick profile stats -->
        <div class="card">
            <div class="card-body">
                <h5 class="card-title">Your Stats</h5>
                <p><strong>Profile Completeness:</strong> 
                   <span class="badge bg-{% if profile_completeness > 80 %}success{% elif profile_completeness > 50 %}warning{% else %}danger{% endif %}">
                       {{ profile_completeness }}%
                   </span>
                </p>
                <p><strong>Matches:</strong> {{ match_count }}</p>
                <p><strong>Swipes Today:</strong> {{ swipes_today }}</p>
            </div>
        </div>
    </div>

    <div class="col-md-6">
        <!-- Main swipe area -->
        <h2>Potential Matches</h2>

        {% if potential_matches %}
            <div class="row">
            {% for match in potential_matches %}
                <div class="col-lg-6 mb-4">
                    <div class="card profile-card">
                        {% if match.profile_picture %}
                        <img src="{{ match.profile_picture.url }}" class="card-img-top" alt="{{ match.user.username }}" style="height: 200px; object-fit: cover;">
                        {% else %}
                        <div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height: 200px;">
                            <span class="text-white">No Photo</span>
                        </div>
                        {% endif %}

                        <div class="card-body">
                            <h5 class="card-title">{{ match.user.get_full_name|default:match.user.username }}</h5>
                            <p class="card-text">
                                <small class="text-muted">{{ match.age }} • {{ match.city }}</small>
                            </p>
                            <p class="card-text">{{ match.bio|truncatewords:20 }}</p>

                            {% if match.get_interests_list %}
                            <div class="mb-2">
                                {% for interest in match.get_interests_list|slice:":3" %}
                                <span class="badge bg-light text-dark">{{ interest }}</span>
                                {% endfor %}
                            </div>
                            {% endif %}

                            <div class="d-grid gap-2">
                                <button class="btn btn-success" onclick="swipeRight({{ match.user.id }})">
                                    👍 Like
                                </button>
                                <button class="btn btn-danger" onclick="swipeLeft({{ match.user.id }})">
                                    👎 Pass
                                </button>
                                <button class="btn btn-primary" onclick="window.location.href='{% url 'profiles:profile_detail' match.user.username %}'">
                                    View Profile
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            {% endfor %}
            </div>
        {% else %}
            <div class="alert alert-info">
                <h4>No more profiles to show!</h4>
                <p>You've swiped through everyone in your area. Try adjusting your preferences or check back later.</p>
                <p>Or maybe you're just too picky? 🤔</p>
            </div>
        {% endif %}
    </div>

    <div class="col-md-3">
        <!-- Quick matches sidebar -->
        <div class="card">
            <div class="card-body">
                <h5 class="card-title">Recent Matches</h5>
                {% for match in recent_matches %}
                <div class="d-flex align-items-center mb-2">
                    {% if match.profile_picture %}
                    <img src="{{ match.profile_picture.url }}" class="rounded-circle me-2" width="40" height="40" style="object-fit: cover;">
                    {% else %}
                    <div class="rounded-circle bg-secondary me-2" style="width: 40px; height: 40px;"></div>
                    {% endif %}
                    <div>
                        <strong>{{ match.user.get_full_name|default:match.user.username }}</strong>
                        <br>
                        <small class="text-muted">Matched recently</small>
                    </div>
                </div>
                {% empty %}
                <p class="text-muted">No matches yet. Keep swiping!</p>
                {% endfor %}
            </div>
        </div>
    </div>
</div>
{% endblock %}

Chapter 9: The Swipe Logic - The Heart of the Operation

Now let's implement the actual swipe functionality. This is where the magic happens!

# dating_core/views.py (additional imports and functions)
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.contrib import messages
import json

@login_required
@require_http_methods(["POST"])
def swipe(request):
    """Handle swipe actions - the main event!"""
    data = json.loads(request.body)
    user_id = data.get('user_id')
    swipe_type = data.get('swipe_type')  # 'right', 'left', or 'super'

    if not user_id or not swipe_type:
        return JsonResponse({'error': 'Missing parameters'}, status=400)

    try:
        swiped_user = User.objects.get(id=user_id)
    except User.DoesNotExist:
        return JsonResponse({'error': 'User not found'}, status=404)

    # Can't swipe on yourself - that's just sad
    if swiped_user == request.user:
        return JsonResponse({'error': 'You cannot swipe on yourself'}, status=400)

    # Check if already swiped
    existing_swipe = Swipe.objects.filter(
        swiper=request.user,
        swiped_on=swiped_user
    ).first()

    if existing_swipe:
        return JsonResponse({'error': 'Already swiped on this user'}, status=400)

    # Create the swipe
    swipe = Swipe.objects.create(
        swiper=request.user,
        swiped_on=swiped_user,
        swipe_type=swipe_type
    )

    # Check for a match! This is the exciting part
    match = None
    if swipe_type in ['right', 'super']:
        # See if the other person also swiped right on you
        reciprocal_swipe = Swipe.objects.filter(
            swiper=swiped_user,
            swiped_on=request.user,
            swipe_type__in=['right', 'super']
        ).first()

        if reciprocal_swipe:
            # It's a match! Create the match record
            match = Match.objects.create(
                user1=min(request.user, swiped_user, key=lambda u: u.id),
                user2=max(request.user, swiped_user, key=lambda u: u.id)
            )

    return JsonResponse({
        'success': True,
        'swipe_type': swipe_type,
        'match': bool(match),
        'match_user': swiped_user.username if match else None,
        'match_profile_picture': swiped_user.profile.profile_picture.url if match and swiped_user.profile.profile_picture else None
    })

@login_required
def get_next_profiles(request):
    """Get the next batch of profiles for swiping"""
    # Get users that haven't been swiped on yet
    swiped_users = Swipe.objects.filter(
        swiper=request.user
    ).values_list('swiped_on', flat=True)

    # Exclude yourself and already swiped users
    potential_matches = Profile.objects.exclude(
        user=request.user
    ).exclude(
        user__in=swiped_users
    )[:5]  # Get 5 at a time

    # Serialize the data
    profiles_data = []
    for profile in potential_matches:
        profiles_data.append({
            'id': profile.user.id,
            'username': profile.user.username,
            'full_name': profile.user.get_full_name() or profile.user.username,
            'age': profile.age(),
            'city': profile.city,
            'bio': profile.bio,
            'profile_picture': profile.profile_picture.url if profile.profile_picture else None,
            'interests': profile.get_interests_list()[:3]
        })

    return JsonResponse({'profiles': profiles_data})

Now let's update our dashboard view to include more data:

# dating_core/views.py (updated dashboard)
@login_required
def dashboard(request):
    """The main dashboard - where the magic happens"""
    user_profile = get_object_or_404(Profile, user=request.user)

    # Calculate profile completeness (because nagging users works)
    completeness_fields = [
        user_profile.bio,
        user_profile.birth_date,
        user_profile.city,
        user_profile.profile_picture,
        user_profile.interests
    ]
    filled_fields = sum(1 for field in completeness_fields if field)
    profile_completeness = int((filled_fields / len(completeness_fields)) * 100)

    # Get potential matches
    swiped_users = Swipe.objects.filter(
        swiper=request.user
    ).values_list('swiped_on', flat=True)

    potential_matches = Profile.objects.exclude(
        user=request.user
    ).exclude(
        user__in=swiped_users
    )[:6]

    # Get recent matches
    user_matches = Match.objects.filter(
        is_active=True
    ).filter(
        models.Q(user1=request.user) | models.Q(user2=request.user)
    )

    recent_matches = []
    for match in user_matches.order_by('-matched_at')[:5]:
        other_user = match.user2 if match.user1 == request.user else match.user1
        recent_matches.append(other_user.profile)

    # Count swipes today
    from datetime import date
    swipes_today = Swipe.objects.filter(
        swiper=request.user,
        swiped_at__date=date.today()
    ).count()

    context = {
        'profile': user_profile,
        'potential_matches': potential_matches,
        'recent_matches': recent_matches,
        'profile_completeness': profile_completeness,
        'match_count': user_matches.count(),
        'swipes_today': swipes_today,
    }

    return render(request, 'dating_core/dashboard.html', context)

Chapter 10: JavaScript - Making It Smooth

Let's add some JavaScript to make the swiping experience smooth:

// static/js/swipe.js
class SwipeManager {
    constructor() {
        this.currentProfiles = [];
        this.currentIndex = 0;
        this.init();
    }

    init() {
        this.loadProfiles();
        this.setupEventListeners();
    }

    async loadProfiles() {
        try {
            const response = await fetch('/api/profiles/next/');
            const data = await response.json();

            if (data.profiles && data.profiles.length > 0) {
                this.currentProfiles = data.profiles;
                this.currentIndex = 0;
                this.displayCurrentProfile();
            } else {
                this.showNoProfilesMessage();
            }
        } catch (error) {
            console.error('Error loading profiles:', error);
        }
    }

    displayCurrentProfile() {
        if (this.currentIndex >= this.currentProfiles.length) {
            this.loadProfiles(); // Load more profiles
            return;
        }

        const profile = this.currentProfiles[this.currentIndex];
        const profileCard = this.createProfileCard(profile);

        const container = document.getElementById('swipeContainer');
        container.innerHTML = '';
        container.appendChild(profileCard);
    }

    createProfileCard(profile) {
        const card = document.createElement('div');
        card.className = 'card profile-card';
        card.innerHTML = `
            ${profile.profile_picture ? 
                `<img src="${profile.profile_picture}" class="card-img-top" alt="${profile.full_name}" style="height: 400px; object-fit: cover;">` :
                `<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height: 400px;">
                    <span class="text-white">No Photo</span>
                </div>`
            }
            <div class="card-body">
                <h3 class="card-title">${profile.full_name}, ${profile.age}</h3>
                <p class="card-text text-muted">${profile.city || 'Location not set'}</p>
                <p class="card-text">${profile.bio || 'No bio yet.'}</p>
                ${profile.interests && profile.interests.length > 0 ? 
                    `<div class="mb-3">
                        ${profile.interests.map(interest => 
                            `<span class="badge bg-light text-dark me-1">${interest}</span>`
                        ).join('')}
                    </div>` : ''
                }
            </div>
        `;

        return card;
    }

    async handleSwipe(swipeType) {
        const profile = this.currentProfiles[this.currentIndex];

        try {
            const response = await fetch('/swipe/', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRFToken': this.getCsrfToken()
                },
                body: JSON.stringify({
                    user_id: profile.id,
                    swipe_type: swipeType
                })
            });

            const result = await response.json();

            if (result.success) {
                // Handle match
                if (result.match) {
                    this.showMatchPopup(result.match_user, result.match_profile_picture);
                }

                // Move to next profile
                this.currentIndex++;
                this.displayCurrentProfile();
            } else {
                alert('Error: ' + result.error);
            }
        } catch (error) {
            console.error('Error swiping:', error);
            alert('Error processing swipe');
        }
    }

    showMatchPopup(username, profilePicture) {
        const popup = document.getElementById('matchPopup');
        const nameSpan = document.getElementById('matchName');

        nameSpan.textContent = username;
        popup.style.display = 'block';

        // Auto-hide after 5 seconds
        setTimeout(() => {
            popup.style.display = 'none';
        }, 5000);
    }

    showNoProfilesMessage() {
        const container = document.getElementById('swipeContainer');
        container.innerHTML = `
            <div class="alert alert-info text-center">
                <h4>No more profiles to show!</h4>
                <p>You've swiped through everyone in your area. Try adjusting your search criteria.</p>
                <button class="btn btn-primary" onclick="swipeManager.loadProfiles()">Check Again</button>
            </div>
        `;
    }

    setupEventListeners() {
        // Keyboard shortcuts - because we're fancy
        document.addEventListener('keydown', (e) => {
            if (e.key === 'ArrowLeft') {
                this.handleSwipe('left');
            } else if (e.key === 'ArrowRight') {
                this.handleSwipe('right');
            } else if (e.key === 'ArrowUp') {
                this.handleSwipe('super');
            }
        });

        // Touch swipe for mobile
        this.setupTouchEvents();
    }

    setupTouchEvents() {
        // Simplified touch handling - you'd want to implement proper swipe detection
        const container = document.getElementById('swipeContainer');
        let startX = 0;

        container.addEventListener('touchstart', (e) => {
            startX = e.touches[0].clientX;
        });

        container.addEventListener('touchend', (e) => {
            const endX = e.changedTouches[0].clientX;
            const diff = endX - startX;

            if (Math.abs(diff) > 50) { // Minimum swipe distance
                if (diff > 0) {
                    this.handleSwipe('right');
                } else {
                    this.handleSwipe('left');
                }
            }
        });
    }

    getCsrfToken() {
        return document.querySelector('[name=csrfmiddlewaretoken]').value;
    }
}

// Initialize when page loads
document.addEventListener('DOMContentLoaded', () => {
    window.swipeManager = new SwipeManager();
});

Chapter 11: URL Updates

Don't forget to update your URLs:

# dating_core/urls.py
from django.urls import path
from . import views

app_name = 'dating_core'

urlpatterns = [
    path('', views.dashboard, name='dashboard'),
    path('swipe/', views.swipe, name='swipe'),
    path('api/profiles/next/', views.get_next_profiles, name='get_next_profiles'),
]

Chapter 12: Profile Detail View

Let's make the profile pages actually useful:

# profiles/views.py
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required

@login_required
def profile_detail(request, username):
    """View someone's profile in all its glory"""
    user = get_object_or_404(User, username=username)
    profile = get_object_or_404(Profile, user=user)

    # Check if you've already swiped on this person
    existing_swipe = None
    if request.user != user:
        existing_swipe = Swipe.objects.filter(
            swiper=request.user,
            swiped_on=user
        ).first()

    # Check if you're matched
    is_matched = Match.objects.filter(
        is_active=True
    ).filter(
        models.Q(user1=request.user, user2=user) | 
        models.Q(user1=user, user2=request.user)
    ).exists()

    context = {
        'viewed_profile': profile,
        'existing_swipe': existing_swipe,
        'is_matched': is_matched,
        'can_swipe': request.user != user and not existing_swipe and not is_matched
    }

    return render(request, 'profiles/profile_detail.html', context)

And the template:

<!-- templates/profiles/profile_detail.html -->
{% extends 'base.html' %}

{% block content %}
<div class="row">
    <div class="col-md-4">
        <!-- Profile Picture -->
        <div class="card">
            {% if viewed_profile.profile_picture %}
            <img src="{{ viewed_profile.profile_picture.url }}" class="card-img-top" alt="{{ viewed_profile.user.username }}">
            {% else %}
            <div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height: 300px;">
                <span class="text-white">No Photo</span>
            </div>
            {% endif %}

            {% if can_swipe %}
            <div class="card-body">
                <div class="d-grid gap-2">
                    <button class="btn btn-success" onclick="swipeRight({{ viewed_profile.user.id }})">
                        👍 Like
                    </button>
                    <button class="btn btn-danger" onclick="swipeLeft({{ viewed_profile.user.id }})">
                        👎 Pass
                    </button>
                    <button class="btn btn-warning" onclick="swipeSuper({{ viewed_profile.user.id }})">
                        ⭐ Super Like
                    </button>
                </div>
            </div>
            {% elif is_matched %}
            <div class="card-body">
                <div class="alert alert-success text-center">
                    <strong>🎉 It's a Match!</strong>
                    <p>You're connected with {{ viewed_profile.user.username }}</p>
                    <button class="btn btn-primary">Send Message</button>
                </div>
            </div>
            {% elif existing_swipe %}
            <div class="card-body">
                <div class="alert alert-info text-center">
                    <strong>You already swiped {{ existing_swipe.get_swipe_type_display }}</strong>
                </div>
            </div>
            {% endif %}
        </div>
    </div>

    <div class="col-md-8">
        <!-- Profile Details -->
        <div class="card">
            <div class="card-body">
                <h1 class="card-title">{{ viewed_profile.user.get_full_name|default:viewed_profile.user.username }}</h1>
                <p class="text-muted">{{ viewed_profile.age }} • {{ viewed_profile.city }}, {{ viewed_profile.country }}</p>

                <hr>

                <h4>About Me</h4>
                <p>{{ viewed_profile.bio|default:"This person hasn't written a bio yet. How mysterious!" }}</p>

                {% if viewed_profile.get_interests_list %}
                <h4>Interests</h4>
                <div class="mb-3">
                    {% for interest in viewed_profile.get_interests_list %}
                    <span class="badge bg-primary me-1 mb-1">{{ interest }}</span>
                    {% endfor %}
                </div>
                {% endif %}

                <div class="row">
                    <div class="col-md-6">
                        <h5>Details</h5>
                        <ul class="list-unstyled">
                            <li><strong>Gender:</strong> {{ viewed_profile.get_gender_display }}</li>
                            <li><strong>Looking for:</strong> {{ viewed_profile.get_looking_for_display }}</li>
                            {% if viewed_profile.height_cm %}
                            <li><strong>Height:</strong> {{ viewed_profile.height_cm }} cm</li>
                            {% endif %}
                        </ul>
                    </div>
                    <div class="col-md-6">
                        <h5>Lifestyle</h5>
                        <ul class="list-unstyled">
                            <li><strong>Smokes:</strong> {{ viewed_profile.smokes|yesno:"Yes,No" }}</li>
                            <li><strong>Drinks:</strong> {{ viewed_profile.drinks|yesno:"Yes,No" }}</li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
function swipeRight(userId) {
    fetch('/swipe/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': '{{ csrf_token }}'
        },
        body: JSON.stringify({
            user_id: userId,
            swipe_type: 'right'
        })
    }).then(response => response.json())
      .then(data => {
          if (data.success) {
              if (data.match) {
                  showMatchPopup(data.match_user);
              }
              window.location.reload();
          } else {
              alert('Error: ' + data.error);
          }
      });
}

function swipeLeft(userId) {
    // Similar implementation for left swipe
}

function swipeSuper(userId) {
    // Similar implementation for super swipe
}
</script>
{% endblock %}

What We've Built in Part 2:

Boom! Now we're talking! You've just built:

  1. Beautiful templates that don't look like they were designed by a caffeinated squirrel
  2. Real swipe functionality with proper match detection
  3. Smooth user experience with JavaScript and AJAX
  4. Detailed profile pages that actually show useful information
  5. Match notifications that make that satisfying "ding!" (well, visually at least)

Your app now actually works! Users can:

In Part 3, we'll add messaging, advanced search, and make this thing production-ready. But for now, pat yourself on the back—you've built something that could actually help people connect!

Now go test it out! Create some test users, make them swipe on each other, and watch the matches roll in. Just remember: if you test it too much and actually get a date, you're doing it right!

How to Build a Dating Website: Part 3 - Adding Brains and Banter

Or: From Swiping to Actually Talking (Revolutionary, I Know!)

Alright, you beautiful bastard! You've built the swiping, the matching, the whole "see someone hot and tap right" thing. But let's be real—this is like building a car that can only honk. Time to add the engine and the steering wheel! In this part, we're adding messaging, search, and making this thing actually useful.

Chapter 13: Messaging - Where Magic (or Awkwardness) Happens

Let's build a messaging system that doesn't suck. Because "hey" followed by "hey" followed by crickets isn't exactly Shakespearean romance.

# messaging/models.py
from django.db import models
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError

class Conversation(models.Model):
    """A conversation between two users"""
    user1 = models.ForeignKey(
        User, 
        on_delete=models.CASCADE, 
        related_name='conversations_as_user1'
    )
    user2 = models.ForeignKey(
        User, 
        on_delete=models.CASCADE, 
        related_name='conversations_as_user2'
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ['user1', 'user2']
        ordering = ['-updated_at']

    def clean(self):
        if self.user1 == self.user2:
            raise ValidationError("Users cannot have conversations with themselves. That's just sad.")

    def __str__(self):
        return f"Chat: {self.user1.username} & {self.user2.username}"

    def get_other_user(self, current_user):
        """Get the other user in the conversation"""
        return self.user2 if self.user1 == current_user else self.user1

    def get_last_message(self):
        """Get the most recent message"""
        return self.messages.order_by('-sent_at').first()

class Message(models.Model):
    """An actual message in a conversation"""
    conversation = models.ForeignKey(
        Conversation, 
        on_delete=models.CASCADE, 
        related_name='messages'
    )
    sender = models.ForeignKey(
        User, 
        on_delete=models.CASCADE,
        related_name='sent_messages'
    )
    content = models.TextField(max_length=2000)
    sent_at = models.DateTimeField(auto_now_add=True)
    read_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ['sent_at']

    def __str__(self):
        return f"Message from {self.sender.username}: {self.content[:50]}..."

    def mark_as_read(self):
        """Mark message as read"""
        if not self.read_at:
            self.read_at = timezone.now()
            self.save()

Now let's create the messaging views:

# messaging/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.db.models import Q, Count
from .models import Conversation, Message
from profiles.models import Profile, Match
import json

@login_required
def conversations_list(request):
    """Show all conversations for the current user"""
    user_conversations = Conversation.objects.filter(
        Q(user1=request.user) | Q(user2=request.user)
    ).annotate(
        unread_count=Count(
            'messages', 
            filter=Q(messages__read_at__isnull=True) & ~Q(messages__sender=request.user)
        )
    ).order_by('-updated_at')

    # Get matches that don't have conversations yet
    user_matches = Match.objects.filter(
        is_active=True
    ).filter(
        Q(user1=request.user) | Q(user2=request.user)
    )

    matches_without_conversations = []
    for match in user_matches:
        other_user = match.user2 if match.user1 == request.user else match.user1
        has_conversation = Conversation.objects.filter(
            Q(user1=request.user, user2=other_user) | 
            Q(user1=other_user, user2=request.user)
        ).exists()

        if not has_conversation:
            matches_without_conversations.append(other_user.profile)

    context = {
        'conversations': user_conversations,
        'matches_without_conversations': matches_without_conversations,
    }

    return render(request, 'messaging/conversations.html', context)

@login_required
def conversation_detail(request, conversation_id):
    """Show a specific conversation"""
    conversation = get_object_or_404(
        Conversation.objects.filter(
            Q(user1=request.user) | Q(user2=request.user)
        ), 
        id=conversation_id
    )

    # Mark messages as read
    unread_messages = conversation.messages.filter(
        read_at__isnull=True
    ).exclude(sender=request.user)

    for message in unread_messages:
        message.mark_as_read()

    if request.method == 'POST':
        content = request.POST.get('content', '').strip()
        if content:
            Message.objects.create(
                conversation=conversation,
                sender=request.user,
                content=content
            )
            # Update conversation timestamp
            conversation.save()  # This triggers auto_now on updated_at
            return redirect('messaging:conversation_detail', conversation_id=conversation.id)

    messages = conversation.messages.all()[:100]  # Last 100 messages

    context = {
        'conversation': conversation,
        'messages': messages,
        'other_user': conversation.get_other_user(request.user),
    }

    return render(request, 'messaging/conversation_detail.html', context)

@login_required
def start_conversation(request, user_id):
    """Start a new conversation with a match"""
    from django.contrib.auth.models import User

    other_user = get_object_or_404(User, id=user_id)

    # Check if they're actually matched
    is_matched = Match.objects.filter(
        is_active=True
    ).filter(
        Q(user1=request.user, user2=other_user) | 
        Q(user1=other_user, user2=request.user)
    ).exists()

    if not is_matched:
        messages.error(request, "You can only message users you've matched with.")
        return redirect('dating_core:dashboard')

    # Get or create conversation
    conversation, created = Conversation.objects.get_or_create(
        user1=min(request.user, other_user, key=lambda u: u.id),
        user2=max(request.user, other_user, key=lambda u: u.id)
    )

    return redirect('messaging:conversation_detail', conversation_id=conversation.id)

@login_required
def send_message_ajax(request, conversation_id):
    """AJAX endpoint for sending messages"""
    if request.method == 'POST':
        conversation = get_object_or_404(
            Conversation.objects.filter(
                Q(user1=request.user) | Q(user2=request.user)
            ), 
            id=conversation_id
        )

        data = json.loads(request.body)
        content = data.get('content', '').strip()

        if content:
            message = Message.objects.create(
                conversation=conversation,
                sender=request.user,
                content=content
            )
            conversation.save()  # Update timestamp

            return JsonResponse({
                'success': True,
                'message_id': message.id,
                'sent_at': message.sent_at.isoformat(),
            })

    return JsonResponse({'success': False, 'error': 'Invalid request'})

@login_required
def get_new_messages(request, conversation_id):
    """AJAX endpoint for getting new messages"""
    conversation = get_object_or_404(
        Conversation.objects.filter(
            Q(user1=request.user) | Q(user2=request.user)
        ), 
        id=conversation_id
    )

    last_message_id = request.GET.get('last_message_id', 0)

    new_messages = conversation.messages.filter(
        id__gt=last_message_id
    ).exclude(sender=request.user).order_by('sent_at')

    # Mark as read
    for message in new_messages:
        message.mark_as_read()

    messages_data = []
    for message in new_messages:
        messages_data.append({
            'id': message.id,
            'content': message.content,
            'sender': message.sender.username,
            'sent_at': message.sent_at.isoformat(),
            'is_own': message.sender == request.user,
        })

    return JsonResponse({
        'messages': messages_data,
        'has_new': len(messages_data) > 0
    })

Chapter 14: Messaging Templates - Making Chat Look Good

Let's create some sexy chat interfaces:

<!-- templates/messaging/conversations.html -->
{% extends 'base.html' %}

{% block title %}Messages - Digital Wingman{% endblock %}

{% block content %}
<div class="row">
    <div class="col-md-4">
        <div class="card">
            <div class="card-header">
                <h5 class="card-title mb-0">Your Matches</h5>
            </div>
            <div class="card-body p-0">
                {% if conversations %}
                <div class="list-group list-group-flush">
                    {% for conversation in conversations %}
                    <a href="{% url 'messaging:conversation_detail' conversation.id %}" 
                       class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
                        <div class="d-flex align-items-center">
                            {% with other_user=conversation.get_other_user request.user %}
                            {% with profile=other_user.profile %}
                            {% if profile.profile_picture %}
                            <img src="{{ profile.profile_picture.url }}" 
                                 class="rounded-circle me-3" 
                                 width="40" 
                                 height="40" 
                                 style="object-fit: cover;">
                            {% else %}
                            <div class="rounded-circle bg-secondary me-3 d-flex align-items-center justify-content-center" 
                                 style="width: 40px; height: 40px;">
                                <span class="text-white small">{{ other_user.username|first|upper }}</span>
                            </div>
                            {% endif %}
                            <div>
                                <strong>{{ other_user.get_full_name|default:other_user.username }}</strong>
                                <br>
                                <small class="text-muted">
                                    {% with last_message=conversation.get_last_message %}
                                    {{ last_message.content|truncatewords:5|default:"Start a conversation..." }}
                                    {% endwith %}
                                </small>
                            </div>
                            {% endwith %}
                            {% endwith %}
                        </div>
                        {% if conversation.unread_count > 0 %}
                        <span class="badge bg-primary rounded-pill">{{ conversation.unread_count }}</span>
                        {% endif %}
                    </a>
                    {% endfor %}
                </div>
                {% else %}
                <div class="text-center p-4">
                    <p class="text-muted">No conversations yet. Match with someone to start chatting!</p>
                </div>
                {% endif %}
            </div>
        </div>

        {% if matches_without_conversations %}
        <div class="card mt-3">
            <div class="card-header">
                <h6 class="card-title mb-0">New Matches</h6>
            </div>
            <div class="card-body">
                {% for match in matches_without_conversations %}
                <div class="d-flex align-items-center mb-2">
                    {% if match.profile_picture %}
                    <img src="{{ match.profile_picture.url }}" 
                         class="rounded-circle me-2" 
                         width="35" 
                         height="35" 
                         style="object-fit: cover;">
                    {% else %}
                    <div class="rounded-circle bg-secondary me-2" 
                         style="width: 35px; height: 35px;"></div>
                    {% endif %}
                    <div class="flex-grow-1">
                        <strong>{{ match.user.get_full_name|default:match.user.username }}</strong>
                    </div>
                    <a href="{% url 'messaging:start_conversation' match.user.id %}" 
                       class="btn btn-sm btn-primary">Message</a>
                </div>
                {% endfor %}
            </div>
        </div>
        {% endif %}
    </div>

    <div class="col-md-8">
        <div class="card h-100">
            <div class="card-body d-flex align-items-center justify-content-center">
                <div class="text-center text-muted">
                    <h3>💬</h3>
                    <p>Select a conversation to start messaging</p>
                    <p><small>Pro tip: "Hey" is boring. Try something creative!</small></p>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

And the actual conversation view:

<!-- templates/messaging/conversation_detail.html -->
{% extends 'base.html' %}

{% block title %}Chat with {{ other_user.username }} - Digital Wingman{% endblock %}

{% block content %}
<div class="row">
    <div class="col-md-8 mx-auto">
        <div class="card">
            <!-- Chat header -->
            <div class="card-header d-flex align-items-center">
                <a href="{% url 'profiles:profile_detail' other_user.username %}" class="text-decoration-none">
                    {% with profile=other_user.profile %}
                    {% if profile.profile_picture %}
                    <img src="{{ profile.profile_picture.url }}" 
                         class="rounded-circle me-3" 
                         width="45" 
                         height="45" 
                         style="object-fit: cover;">
                    {% else %}
                    <div class="rounded-circle bg-secondary me-3 d-flex align-items-center justify-content-center" 
                         style="width: 45px; height: 45px;">
                        <span class="text-white">{{ other_user.username|first|upper }}</span>
                    </div>
                    {% endif %}
                    {% endwith %}
                </a>
                <div>
                    <h5 class="mb-0">{{ other_user.get_full_name|default:other_user.username }}</h5>
                    <small class="text-muted">
                        {% with profile=other_user.profile %}
                        {{ profile.age }} • {{ profile.city|default:"Unknown location" }}
                        {% endwith %}
                    </small>
                </div>
                <div class="ms-auto">
                    <a href="{% url 'messaging:conversations_list' %}" class="btn btn-sm btn-outline-secondary">
                        Back to Chats
                    </a>
                </div>
            </div>

            <!-- Messages area -->
            <div class="card-body" style="height: 60vh; overflow-y: auto;" id="messagesContainer">
                {% for message in messages %}
                <div class="d-flex mb-3 {% if message.sender == request.user %}justify-content-end{% endif %}">
                    <div class="{% if message.sender == request.user %}bg-primary text-white{% else %}bg-light{% endif %} rounded p-3" 
                         style="max-width: 70%;">
                        {{ message.content|linebreaksbr }}
                        <div class="{% if message.sender == request.user %}text-white-50{% else %}text-muted{% endif %} small mt-1">
                            {{ message.sent_at|timesince }} ago
                        </div>
                    </div>
                </div>
                {% empty %}
                <div class="text-center text-muted py-5">
                    <h4>No messages yet</h4>
                    <p>Break the ice! Send the first message.</p>
                    <p><small>Maybe don't start with "hey" - be creative!</small></p>
                </div>
                {% endfor %}
            </div>

            <!-- Message input -->
            <div class="card-footer">
                <form method="post" id="messageForm">
                    {% csrf_token %}
                    <div class="input-group">
                        <input type="text" 
                               name="content" 
                               class="form-control" 
                               placeholder="Type your message... (Pro tip: 'u up?' rarely works)" 
                               maxlength="2000"
                               required
                               id="messageInput">
                        <button class="btn btn-primary" type="submit">Send</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
// Auto-scroll to bottom of messages
function scrollToBottom() {
    const container = document.getElementById('messagesContainer');
    container.scrollTop = container.scrollHeight;
}

// Send message with AJAX
document.getElementById('messageForm').addEventListener('submit', function(e) {
    e.preventDefault();

    const input = document.getElementById('messageInput');
    const content = input.value.trim();

    if (content) {
        // Create temporary message display
        const messagesContainer = document.getElementById('messagesContainer');
        const messageDiv = document.createElement('div');
        messageDiv.className = 'd-flex mb-3 justify-content-end';
        messageDiv.innerHTML = `
            <div class="bg-primary text-white rounded p-3" style="max-width: 70%;">
                ${content.replace(/\n/g, '<br>')}
                <div class="text-white-50 small mt-1">Just now</div>
            </div>
        `;
        messagesContainer.appendChild(messageDiv);

        // Clear input
        input.value = '';

        // Scroll to bottom
        scrollToBottom();

        // Send via AJAX
        fetch('{% url "messaging:send_message_ajax" conversation.id %}', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRFToken': '{{ csrf_token }}'
            },
            body: JSON.stringify({ content: content })
        }).then(response => response.json())
          .then(data => {
              if (!data.success) {
                  alert('Failed to send message');
              }
          });
    }
});

// Check for new messages periodically
function checkForNewMessages() {
    const lastMessage = document.querySelector('#messagesContainer .d-flex:last-child');
    const lastMessageId = lastMessage ? lastMessage.dataset.messageId : 0;

    fetch(`{% url "messaging:get_new_messages" conversation.id %}?last_message_id=${lastMessageId}`)
        .then(response => response.json())
        .then(data => {
            if (data.has_new) {
                data.messages.forEach(message => {
                    const messagesContainer = document.getElementById('messagesContainer');
                    const messageDiv = document.createElement('div');
                    messageDiv.className = 'd-flex mb-3';
                    messageDiv.innerHTML = `
                        <div class="bg-light rounded p-3" style="max-width: 70%;">
                            ${message.content.replace(/\n/g, '<br>')}
                            <div class="text-muted small mt-1">
                                ${new Date(message.sent_at).toLocaleTimeString()}
                            </div>
                        </div>
                    `;
                    messagesContainer.appendChild(messageDiv);
                });
                scrollToBottom();
            }
        });
}

// Initial scroll to bottom
scrollToBottom();

// Check for new messages every 3 seconds
setInterval(checkForNewMessages, 3000);
</script>
{% endblock %}

Chapter 15: Search and Discovery - Finding Your Needle in the Haystack

No dating app is complete without search. Let's help users find exactly what they're looking for (good luck with that).

# search/views.py
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from profiles.models import Profile
from dating_core.models import Swipe
import datetime

@login_required
def search_profiles(request):
    """Advanced profile search"""
    # Get base queryset (exclude self and already swiped users)
    swiped_users = Swipe.objects.filter(
        swiper=request.user
    ).values_list('swiped_on', flat=True)

    profiles = Profile.objects.exclude(
        user=request.user
    ).exclude(
        user__in=swiped_users
    )

    # Get filter parameters
    gender = request.GET.get('gender')
    min_age = request.GET.get('min_age')
    max_age = request.GET.get('max_age')
    city = request.GET.get('city')
    interests = request.GET.get('interests')
    has_photo = request.GET.get('has_photo')

    # Apply filters
    if gender:
        profiles = profiles.filter(gender=gender)

    if min_age:
        max_birth_date = datetime.date.today() - datetime.timedelta(days=int(min_age)*365)
        profiles = profiles.filter(birth_date__lte=max_birth_date)

    if max_age:
        min_birth_date = datetime.date.today() - datetime.timedelta(days=int(max_age)*365)
        profiles = profiles.filter(birth_date__gte=min_birth_date)

    if city:
        profiles = profiles.filter(city__icontains=city)

    if interests:
        interest_list = [interest.strip() for interest in interests.split(',')]
        for interest in interest_list:
            profiles = profiles.filter(interests__icontains=interest)

    if has_photo:
        profiles = profiles.exclude(profile_picture='')

    # Get unique cities for the search form
    popular_cities = Profile.objects.exclude(city='').values_list('city', flat=True).distinct()[:10]

    context = {
        'profiles': profiles[:50],  # Limit results
        'popular_cities': popular_cities,
        'search_performed': any([gender, min_age, max_age, city, interests, has_photo]),
        'form_data': request.GET,
    }

    return render(request, 'search/search.html', context)

@login_required
def discovery_feed(request):
    """Smart discovery feed with suggested profiles"""
    user_profile = Profile.objects.get(user=request.user)

    # Get already swiped users
    swiped_users = Swipe.objects.filter(
        swiper=request.user
    ).values_list('swiped_on', flat=True)

    # Base queryset
    profiles = Profile.objects.exclude(
        user=request.user
    ).exclude(
        user__in=swiped_users
    )

    # Filter by preferences
    profiles = profiles.filter(gender=user_profile.looking_for)

    # Age-based suggestions (prefer similar age)
    user_age = user_profile.age()
    if user_age:
        age_range = 5  # ±5 years
        min_birth_date = datetime.date.today() - datetime.timedelta(days=(user_age + age_range)*365)
        max_birth_date = datetime.date.today() - datetime.timedelta(days=(user_age - age_range)*365)
        profiles = profiles.filter(birth_date__range=[min_birth_date, max_birth_date])

    # Location-based suggestions (same city first)
    if user_profile.city:
        same_city_profiles = profiles.filter(city=user_profile.city)
        other_city_profiles = profiles.exclude(city=user_profile.city)
        profiles = list(same_city_profiles) + list(other_city_profiles)

    # Interest-based matching (bonus points for shared interests)
    user_interests = user_profile.get_interests_list()
    scored_profiles = []

    for profile in profiles:
        score = 0
        profile_interests = profile.get_interests_list()

        # Score based on shared interests
        shared_interests = set(user_interests) & set(profile_interests)
        score += len(shared_interests) * 10

        # Bonus for having a profile picture
        if profile.profile_picture:
            score += 5

        # Bonus for complete profile
        if profile.bio and len(profile.bio) > 50:
            score += 3

        scored_profiles.append((profile, score))

    # Sort by score and take top profiles
    scored_profiles.sort(key=lambda x: x[1], reverse=True)
    suggested_profiles = [profile for profile, score in scored_profiles[:20]]

    context = {
        'suggested_profiles': suggested_profiles,
        'user_profile': user_profile,
    }

    return render(request, 'search/discovery.html', context)

Search template:

<!-- templates/search/search.html -->
{% extends 'base.html' %}

{% block title %}Search - Digital Wingman{% endblock %}

{% block content %}
<div class="row">
    <div class="col-md-3">
        <!-- Search filters -->
        <div class="card">
            <div class="card-header">
                <h5 class="card-title mb-0">Refine Your Search</h5>
            </div>
            <div class="card-body">
                <form method="get" id="searchForm">
                    <!-- Gender -->
                    <div class="mb-3">
                        <label class="form-label">Gender</label>
                        <select name="gender" class="form-select">
                            <option value="">Any gender</option>
                            <option value="M" {% if form_data.gender == 'M' %}selected{% endif %}>Male</option>
                            <option value="F" {% if form_data.gender == 'F' %}selected{% endif %}>Female</option>
                            <option value="NB" {% if form_data.gender == 'NB' %}selected{% endif %}>Non-binary</option>
                            <option value="O" {% if form_data.gender == 'O' %}selected{% endif %}>Other</option>
                        </select>
                    </div>

                    <!-- Age range -->
                    <div class="mb-3">
                        <label class="form-label">Age Range</label>
                        <div class="row">
                            <div class="col">
                                <input type="number" name="min_age" class="form-control" placeholder="Min" 
                                       min="18" max="100" value="{{ form_data.min_age }}">
                            </div>
                            <div class="col">
                                <input type="number" name="max_age" class="form-control" placeholder="Max" 
                                       min="18" max="100" value="{{ form_data.max_age }}">
                            </div>
                        </div>
                    </div>

                    <!-- Location -->
                    <div class="mb-3">
                        <label class="form-label">City</label>
                        <input type="text" name="city" class="form-control" placeholder="Enter city" 
                               value="{{ form_data.city }}" list="citySuggestions">
                        <datalist id="citySuggestions">
                            {% for city in popular_cities %}
                            <option value="{{ city }}">
                            {% endfor %}
                        </datalist>
                    </div>

                    <!-- Interests -->
                    <div class="mb-3">
                        <label class="form-label">Interests</label>
                        <input type="text" name="interests" class="form-control" 
                               placeholder="e.g. hiking, reading, coffee" value="{{ form_data.interests }}">
                        <div class="form-text">Separate multiple interests with commas</div>
                    </div>

                    <!-- Photo filter -->
                    <div class="mb-3">
                        <div class="form-check">
                            <input class="form-check-input" type="checkbox" name="has_photo" 
                                   id="hasPhoto" {% if form_data.has_photo %}checked{% endif %}>
                            <label class="form-check-label" for="hasPhoto">
                                Only show profiles with photos
                            </label>
                        </div>
                    </div>

                    <button type="submit" class="btn btn-primary w-100">Search</button>
                    <a href="{% url 'search:search_profiles' %}" class="btn btn-outline-secondary w-100 mt-2">
                        Clear Filters
                    </a>
                </form>
            </div>
        </div>
    </div>

    <div class="col-md-9">
        <!-- Search results -->
        <div class="d-flex justify-content-between align-items-center mb-4">
            <h2>Search Results</h2>
            <span class="text-muted">{{ profiles|length }} profiles found</span>
        </div>

        {% if search_performed and not profiles %}
        <div class="alert alert-info">
            <h4>No profiles found</h4>
            <p>Try adjusting your search criteria. Maybe you're being too picky? 🤔</p>
        </div>
        {% elif not search_performed %}
        <div class="alert alert-light text-center">
            <h4>Ready to find your perfect match?</h4>
            <p>Use the filters to narrow down your search, or <a href="{% url 'search:discovery_feed' %}">check out our smart suggestions</a>.</p>
        </div>
        {% endif %}

        <div class="row">
            {% for profile in profiles %}
            <div class="col-lg-4 col-md-6 mb-4">
                <div class="card profile-card h-100">
                    {% if profile.profile_picture %}
                    <img src="{{ profile.profile_picture.url }}" class="card-img-top" 
                         alt="{{ profile.user.username }}" style="height: 200px; object-fit: cover;">
                    {% else %}
                    <div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" 
                         style="height: 200px;">
                        <span class="text-white">No Photo</span>
                    </div>
                    {% endif %}

                    <div class="card-body d-flex flex-column">
                        <h5 class="card-title">{{ profile.user.get_full_name|default:profile.user.username }}</h5>
                        <p class="card-text">
                            <small class="text-muted">{{ profile.age }} • {{ profile.city|default:"Unknown" }}</small>
                        </p>
                        <p class="card-text flex-grow-1">{{ profile.bio|truncatewords:15 }}</p>

                        {% if profile.get_interests_list %}
                        <div class="mb-2">
                            {% for interest in profile.get_interests_list|slice:":2" %}
                            <span class="badge bg-light text-dark">{{ interest }}</span>
                            {% endfor %}
                            {% if profile.get_interests_list|length > 2 %}
                            <span class="badge bg-light text-dark">+{{ profile.get_interests_list|length|add:"-2" }} more</span>
                            {% endif %}
                        </div>
                        {% endif %}

                        <div class="d-grid gap-2 mt-auto">
                            <button class="btn btn-success" 
                                    onclick="swipeRight({{ profile.user.id }})">
                                👍 Like
                            </button>
                            <button class="btn btn-danger" 
                                    onclick="swipeLeft({{ profile.user.id }})">
                                👎 Pass
                            </button>
                            <a href="{% url 'profiles:profile_detail' profile.user.username %}" 
                               class="btn btn-outline-primary">
                                View Profile
                            </a>
                        </div>
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
    </div>
</div>
{% endblock %}

Chapter 16: URLs and App Configuration

Don't forget to wire everything up:

# messaging/urls.py
from django.urls import path
from . import views

app_name = 'messaging'

urlpatterns = [
    path('', views.conversations_list, name='conversations_list'),
    path('conversation/<int:conversation_id>/', views.conversation_detail, name='conversation_detail'),
    path('start/<int:user_id>/', views.start_conversation, name='start_conversation'),
    path('api/send/<int:conversation_id>/', views.send_message_ajax, name='send_message_ajax'),
    path('api/messages/<int:conversation_id>/', views.get_new_messages, name='get_new_messages'),
]

# search/urls.py
from django.urls import path
from . import views

app_name = 'search'

urlpatterns = [
    path('', views.search_profiles, name='search_profiles'),
    path('discovery/', views.discovery_feed, name='discovery_feed'),
]

# Update main urls.py
# digital_wingman/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('dating_core.urls')),
    path('profiles/', include('profiles.urls')),
    path('messages/', include('messaging.urls')),
    path('search/', include('search.urls')),
    path('accounts/', include('django.contrib.auth.urls')),
]

What We've Built in Part 3:

Holy relationship goals, Batman! You've just built:

  1. A full messaging system with real-time updates
  2. Smart conversation management with read receipts
  3. Advanced search functionality with filters
  4. Intelligent discovery feed that suggests matches
  5. Beautiful chat interfaces that don't suck

Your app now has:

In Part 4, we'll make this thing production-ready with user authentication, security, performance optimizations, and deployment. But for now, you've built something that could actually help people find love (or at least a decent conversation)!

Go ahead—test the messaging, try the search, and pat yourself on the back. You're not just building a dating app anymore; you're building relationships!

Just remember: if you test the messaging too much and end up in a meaningful relationship, you're definitely doing it right! 🎉

How to Build a Dating Website: Part 4 - Making It Bulletproof

Or: From "It Works on My Machine" to "Holy Crap, This is Actually Good"

Alright, you magnificent code warrior! You've built the swiping, the matching, the messaging—all the fun stuff. But right now, your app has about as much security as a screen door on a submarine. Let's fix that and make this thing ready for actual humans (you know, the kind that will try to break everything).

Chapter 17: User Authentication & Registration - The Digital Bouncer

Before we let anyone into our exclusive club, we need a proper registration system. Because "admin/admin" isn't exactly enterprise-grade security.

The Registration View - More Than Just a Form

Let's build a registration system that actually validates stuff and doesn't let 12-year-olds sign up (unless they're really, really mature 12-year-olds).

# accounts/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login, authenticate
from django.contrib.auth.models import User
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.utils import timezone
import datetime
import re

def validate_password_strength(password):
    """
    Validate that password is strong enough.
    Because 'password123' isn't cutting it anymore.
    """
    if len(password) < 8:
        raise ValidationError("Password must be at least 8 characters long. Make it memorable!")

    if not re.search(r'[A-Z]', password):
        raise ValidationError("Password must contain at least one uppercase letter.")

    if not re.search(r'[a-z]', password):
        raise ValidationError("Password must contain at least one lowercase letter.")

    if not re.search(r'[0-9]', password):
        raise ValidationError("Password must contain at least one number.")

    if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
        raise ValidationError("Password must contain at least one special character.")

def validate_birth_date(birth_date):
    """Validate that user is at least 18 years old. Because we're not creeps."""
    today = timezone.now().date()
    age = today.year - birth_date.year - ((today.month, today.day) < (birth_date.month, birth_date.day))

    if age < 18:
        raise ValidationError("You must be at least 18 years old to use this service.")

    if age > 100:
        raise ValidationError("Please enter a valid birth date.")

def register(request):
    """
    User registration view that actually cares about security and validation.
    This is the bouncer at our digital nightclub.
    """
    if request.method == 'POST':
        # Extract form data
        username = request.POST.get('username', '').strip()
        email = request.POST.get('email', '').strip()
        password = request.POST.get('password', '')
        password_confirm = request.POST.get('password_confirm', '')
        first_name = request.POST.get('first_name', '').strip()
        last_name = request.POST.get('last_name', '').strip()

        # Basic profile info
        gender = request.POST.get('gender')
        looking_for = request.POST.get('looking_for')
        birth_date_str = request.POST.get('birth_date')

        errors = []

        # Validate required fields
        if not all([username, email, password, gender, looking_for, birth_date_str]):
            errors.append("All fields are required. Yes, even that one.")

        # Check if username already exists
        if User.objects.filter(username=username).exists():
            errors.append("Username already taken. Get more creative!")

        # Check if email already exists
        if User.objects.filter(email=email).exists():
            errors.append("Email already registered. Did you forget you signed up?")

        # Validate password strength
        try:
            validate_password_strength(password)
        except ValidationError as e:
            errors.append(str(e))

        # Check password confirmation
        if password != password_confirm:
            errors.append("Passwords don't match. You had one job!")

        # Validate birth date
        birth_date = None
        if birth_date_str:
            try:
                birth_date = datetime.datetime.strptime(birth_date_str, '%Y-%m-%d').date()
                validate_birth_date(birth_date)
            except ValueError:
                errors.append("Please enter a valid birth date.")
            except ValidationError as e:
                errors.append(str(e))

        # If no errors, create the user
        if not errors:
            try:
                # Create user account
                user = User.objects.create_user(
                    username=username,
                    email=email,
                    password=password,
                    first_name=first_name,
                    last_name=last_name
                )

                # Create profile
                from profiles.models import Profile
                profile = Profile.objects.create(
                    user=user,
                    gender=gender,
                    looking_for=looking_for,
                    birth_date=birth_date
                )

                # Log the user in
                login(request, user)

                messages.success(request, 
                    f"Welcome to Digital Wingman, {first_name or username}! "
                    f"Complete your profile to get more matches."
                )

                return redirect('profiles:profile_edit')

            except Exception as e:
                errors.append(f"An error occurred during registration: {str(e)}")

        # If there were errors, show them
        for error in errors:
            messages.error(request, error)

    # Render registration form
    return render(request, 'accounts/register.html')

Now let's break down what we're doing here:

  1. Password Validation: We're not just checking length—we're ensuring mixed case, numbers, and special characters. Because "password123" deserves to be shamed.

  2. Age Verification: We're checking that users are actually adults. This isn't just good practice—it's legally required in most places.

  3. Duplicate Prevention: We check for existing usernames and emails to avoid account confusion.

  4. Error Handling: We collect all errors and show them to the user, so they don't have to play "guess what's wrong" with the form.

The Registration Template - Making Signup Less Painful

Now let's create a registration form that guides users through the process without making them want to throw their computer out the window.

<!-- templates/accounts/register.html -->
{% extends 'base.html' %}

{% block title %}Join Digital Wingman - Find Your Perfect Match{% endblock %}

{% block content %}
<div class="row justify-content-center">
    <div class="col-md-8 col-lg-6">
        <div class="card">
            <div class="card-header text-center">
                <h2 class="mb-0">Join Digital Wingman</h2>
                <p class="text-muted mb-0">Find your perfect match today</p>
            </div>

            <div class="card-body">
                <!-- Progress indicator -->
                <div class="mb-4">
                    <div class="d-flex justify-content-between text-muted small">
                        <span>Account Info</span>
                        <span>Profile Details</span>
                        <span>Preferences</span>
                    </div>
                    <div class="progress" style="height: 4px;">
                        <div class="progress-bar" style="width: 33%;"></div>
                    </div>
                </div>

                <form method="post" id="registrationForm" novalidate>
                    {% csrf_token %}

                    <div class="row">
                        <!-- Basic Account Information -->
                        <div class="col-md-6">
                            <h5 class="border-bottom pb-2 mb-3">Account Information</h5>

                            <!-- Username -->
                            <div class="mb-3">
                                <label for="username" class="form-label">Username *</label>
                                <input type="text" 
                                       class="form-control" 
                                       id="username" 
                                       name="username" 
                                       value="{{ request.POST.username }}"
                                       required
                                       maxlength="150">
                                <div class="form-text">
                                    This is how other users will see you. Choose wisely!
                                </div>
                                <div class="invalid-feedback" id="usernameFeedback">
                                    Please choose a username.
                                </div>
                            </div>

                            <!-- Email -->
                            <div class="mb-3">
                                <label for="email" class="form-label">Email Address *</label>
                                <input type="email" 
                                       class="form-control" 
                                       id="email" 
                                       name="email" 
                                       value="{{ request.POST.email }}"
                                       required>
                                <div class="form-text">
                                    We'll never share your email. Pinky promise.
                                </div>
                            </div>

                            <!-- Password -->
                            <div class="mb-3">
                                <label for="password" class="form-label">Password *</label>
                                <input type="password" 
                                       class="form-control" 
                                       id="password" 
                                       name="password" 
                                       required
                                       minlength="8">
                                <div class="form-text">
                                    Must be at least 8 characters with uppercase, lowercase, number, and special character.
                                </div>
                                <div class="password-strength mt-2">
                                    <div class="progress" style="height: 5px;">
                                        <div class="progress-bar" id="passwordStrength" style="width: 0%;"></div>
                                    </div>
                                    <small class="text-muted" id="passwordFeedback">Password strength</small>
                                </div>
                            </div>

                            <!-- Confirm Password -->
                            <div class="mb-3">
                                <label for="password_confirm" class="form-label">Confirm Password *</label>
                                <input type="password" 
                                       class="form-control" 
                                       id="password_confirm" 
                                       name="password_confirm" 
                                       required
                                       minlength="8">
                                <div class="invalid-feedback" id="passwordMatchFeedback">
                                    Passwords don't match.
                                </div>
                            </div>
                        </div>

                        <!-- Personal Information -->
                        <div class="col-md-6">
                            <h5 class="border-bottom pb-2 mb-3">About You</h5>

                            <!-- Name -->
                            <div class="row">
                                <div class="col-md-6 mb-3">
                                    <label for="first_name" class="form-label">First Name</label>
                                    <input type="text" 
                                           class="form-control" 
                                           id="first_name" 
                                           name="first_name" 
                                           value="{{ request.POST.first_name }}">
                                </div>
                                <div class="col-md-6 mb-3">
                                    <label for="last_name" class="form-label">Last Name</label>
                                    <input type="text" 
                                           class="form-control" 
                                           id="last_name" 
                                           name="last_name" 
                                           value="{{ request.POST.last_name }}">
                                    <div class="form-text">
                                        Last name is optional and won't be publicly displayed.
                                    </div>
                                </div>
                            </div>

                            <!-- Birth Date -->
                            <div class="mb-3">
                                <label for="birth_date" class="form-label">Birth Date *</label>
                                <input type="date" 
                                       class="form-control" 
                                       id="birth_date" 
                                       name="birth_date" 
                                       value="{{ request.POST.birth_date }}"
                                       required
                                       max="{{ today|date:'Y-m-d' }}">
                                <div class="form-text">
                                    You must be at least 18 years old to register.
                                </div>
                            </div>

                            <!-- Gender -->
                            <div class="mb-3">
                                <label for="gender" class="form-label">I am *</label>
                                <select class="form-select" id="gender" name="gender" required>
                                    <option value="">Select your gender</option>
                                    <option value="M" {% if request.POST.gender == 'M' %}selected{% endif %}>Male</option>
                                    <option value="F" {% if request.POST.gender == 'F' %}selected{% endif %}>Female</option>
                                    <option value="NB" {% if request.POST.gender == 'NB' %}selected{% endif %}>Non-binary</option>
                                    <option value="O" {% if request.POST.gender == 'O' %}selected{% endif %}>Other</option>
                                </select>
                            </div>

                            <!-- Looking For -->
                            <div class="mb-3">
                                <label for="looking_for" class="form-label">I'm looking for *</label>
                                <select class="form-select" id="looking_for" name="looking_for" required>
                                    <option value="">Select preference</option>
                                    <option value="M" {% if request.POST.looking_for == 'M' %}selected{% endif %}>Men</option>
                                    <option value="F" {% if request.POST.looking_for == 'F' %}selected{% endif %}>Women</option>
                                    <option value="NB" {% if request.POST.looking_for == 'NB' %}selected{% endif %}>Non-binary</option>
                                    <option value="O" {% if request.POST.looking_for == 'O' %}selected{% endif %}>Other</option>
                                    <option value="A" {% if request.POST.looking_for == 'A' %}selected{% endif %}>Everyone</option>
                                </select>
                            </div>
                        </div>
                    </div>

                    <!-- Terms and Conditions -->
                    <div class="mb-3">
                        <div class="form-check">
                            <input class="form-check-input" type="checkbox" id="terms" required>
                            <label class="form-check-label" for="terms">
                                I agree to the <a href="#" target="_blank">Terms of Service</a> 
                                and <a href="#" target="_blank">Privacy Policy</a>. 
                                I confirm that I am at least 18 years old.
                            </label>
                            <div class="invalid-feedback">
                                You must agree to the terms and conditions.
                            </div>
                        </div>
                    </div>

                    <!-- Submit Button -->
                    <div class="d-grid">
                        <button type="submit" class="btn btn-primary btn-lg">
                            Create My Account
                        </button>
                    </div>

                    <!-- Login Link -->
                    <div class="text-center mt-3">
                        <p class="mb-0">
                            Already have an account? 
                            <a href="{% url 'login' %}" class="text-decoration-none">Sign in here</a>
                        </p>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
// Real-time form validation
document.addEventListener('DOMContentLoaded', function() {
    const form = document.getElementById('registrationForm');
    const password = document.getElementById('password');
    const passwordConfirm = document.getElementById('password_confirm');
    const passwordStrength = document.getElementById('passwordStrength');
    const passwordFeedback = document.getElementById('passwordFeedback');

    // Password strength indicator
    password.addEventListener('input', function() {
        const value = password.value;
        let strength = 0;
        let feedback = '';

        // Length check
        if (value.length >= 8) strength += 25;

        // Complexity checks
        if (/[A-Z]/.test(value)) strength += 25;
        if (/[a-z]/.test(value)) strength += 25;
        if (/[0-9!@#$%^&*(),.?":{}|<>]/.test(value)) strength += 25;

        // Update progress bar
        passwordStrength.style.width = strength + '%';

        // Update feedback text and color
        if (strength < 50) {
            passwordStrength.className = 'progress-bar bg-danger';
            feedback = 'Weak - Add more character types';
        } else if (strength < 75) {
            passwordStrength.className = 'progress-bar bg-warning';
            feedback = 'Medium - Getting better';
        } else {
            passwordStrength.className = 'progress-bar bg-success';
            feedback = 'Strong - Nice job!';
        }

        passwordFeedback.textContent = feedback;
    });

    // Password confirmation check
    passwordConfirm.addEventListener('input', function() {
        if (password.value !== passwordConfirm.value) {
            passwordConfirm.classList.add('is-invalid');
        } else {
            passwordConfirm.classList.remove('is-invalid');
        }
    });

    // Age validation
    const birthDate = document.getElementById('birth_date');
    birthDate.addEventListener('change', function() {
        const selectedDate = new Date(this.value);
        const today = new Date();
        const age = today.getFullYear() - selectedDate.getFullYear();

        if (age < 18) {
            this.classList.add('is-invalid');
        } else {
            this.classList.remove('is-invalid');
        }
    });

    // Form submission validation
    form.addEventListener('submit', function(event) {
        if (!form.checkValidity()) {
            event.preventDefault();
            event.stopPropagation();
        }
        form.classList.add('was-validated');
    });
});
</script>
{% endblock %}

Why This Registration System Rocks:

  1. Progressive Disclosure: We split the form into logical sections so users don't get overwhelmed.

  2. Real-time Validation: Users get immediate feedback about password strength, age requirements, etc.

  3. Clear Instructions: Every field explains why we need the information and how it will be used.

  4. Accessibility: Proper labels, error messages, and form structure make it usable for everyone.

Chapter 18: Profile Completion & Edit - Making Users Actually Interesting

Now that users can register, let's make sure they complete their profiles. Because a profile with just a username is about as appealing as a pizza with no cheese.

# profiles/views.py (additional functions)
@login_required
def profile_edit(request):
    """
    Edit profile view with comprehensive validation and helpful guidance.
    This turns blank profiles into dating masterpieces.
    """
    profile = get_object_or_404(Profile, user=request.user)

    if request.method == 'POST':
        # Handle profile picture upload separately
        if 'profile_picture' in request.FILES:
            profile_picture = request.FILES['profile_picture']
            # Validate image
            if profile_picture.size > 5 * 1024 * 1024:  # 5MB limit
                messages.error(request, "Image size must be less than 5MB.")
            elif not profile_picture.content_type.startswith('image/'):
                messages.error(request, "Please upload a valid image file.")
            else:
                profile.profile_picture = profile_picture
                profile.save()
                messages.success(request, "Profile picture updated successfully!")
            return redirect('profiles:profile_edit')

        # Handle other profile data
        form_data = request.POST

        # Basic validation
        errors = []

        # Bio validation
        bio = form_data.get('bio', '').strip()
        if len(bio) > 500:
            errors.append("Bio must be less than 500 characters. Brevity is the soul of wit!")

        # Interests validation
        interests = form_data.get('interests', '').strip()
        if interests:
            interest_list = [interest.strip() for interest in interests.split(',')]
            if len(interest_list) > 10:
                errors.append("Please limit yourself to 10 interests. We get it, you're interesting!")

        # Location validation
        city = form_data.get('city', '').strip()
        if city and len(city) > 100:
            errors.append("City name is too long. Where do you really live?")

        # Height validation
        height_cm = form_data.get('height_cm')
        if height_cm:
            try:
                height_cm = int(height_cm)
                if height_cm < 100 or height_cm > 250:
                    errors.append("Please enter a valid height between 100cm and 250cm.")
            except ValueError:
                errors.append("Height must be a number.")

        if not errors:
            # Update profile fields
            profile.bio = bio
            profile.interests = interests
            profile.city = city
            profile.country = form_data.get('country', '')

            if height_cm:
                profile.height_cm = height_cm

            profile.smokes = form_data.get('smokes') == 'on'
            profile.drinks = form_data.get('drinks') == 'on'

            profile.save()

            # Calculate new completeness score
            completeness = profile.calculate_completeness()

            messages.success(request, 
                f"Profile updated successfully! "
                f"Your profile is {completeness}% complete."
            )

            return redirect('profiles:profile_detail', username=request.user.username)

        else:
            for error in errors:
                messages.error(request, error)

    # Calculate current completeness
    completeness = profile.calculate_completeness()

    # Suggest what to add next
    suggestions = []
    if not profile.bio:
        suggestions.append("Add a bio to tell people about yourself")
    if not profile.profile_picture:
        suggestions.append("Upload a profile picture - profiles with photos get 10x more matches")
    if not profile.interests:
        suggestions.append("Add some interests to help people find common ground")
    if not profile.city:
        suggestions.append("Add your location to find people nearby")

    context = {
        'profile': profile,
        'completeness': completeness,
        'suggestions': suggestions,
    }

    return render(request, 'profiles/profile_edit.html', context)

def calculate_completeness(self):
    """Calculate how complete a profile is"""
    fields_to_check = [
        self.bio,
        self.profile_picture,
        self.interests,
        self.city,
        self.birth_date
    ]

    filled_fields = sum(1 for field in fields_to_check if field)
    return int((filled_fields / len(fields_to_check)) * 100)

Key Features of Our Profile System:

  1. Image Validation: We check file size and type to prevent users from uploading their entire photo library.

  2. Smart Suggestions: The system suggests what to add next based on what's missing.

  3. Progress Tracking: Users see their completion percentage, which is great for motivation.

  4. Input Validation: We validate everything from bio length to reasonable height ranges.

Chapter 19: Security Middleware - The Digital Bodyguard

Let's add some security middleware to protect our app from common attacks. Because the internet is full of people with too much time and not enough morals.

# security/middleware.py
from django.utils.deprecation import MiddlewareMixin
from django.conf import settings
from django.http import HttpResponseForbidden
import re

class SecurityMiddleware(MiddlewareMixin):
    """
    Custom security middleware to protect against common attacks.
    This is the bouncer that checks IDs at the door.
    """

    def process_request(self, request):
        # Add security headers
        self.add_security_headers(request)

        # Validate request data
        if not self.validate_request(request):
            return HttpResponseForbidden("Suspicious activity detected.")

        return None

    def add_security_headers(self, request):
        """Add security headers to responses"""
        # This will be applied in process_response
        pass

    def process_response(self, request, response):
        """Add security headers to every response"""
        # Prevent clickjacking
        response['X-Frame-Options'] = 'DENY'

        # Enable XSS protection
        response['X-XSS-Protection'] = '1; mode=block'

        # Prevent MIME type sniffing
        response['X-Content-Type-Options'] = 'nosniff'

        # Referrer policy
        response['Referrer-Policy'] = 'strict-origin-when-cross-origin'

        # Content Security Policy (basic)
        csp = [
            "default-src 'self'",
            "script-src 'self' https://cdn.jsdelivr.net",
            "style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'",
            "img-src 'self' data: https:",
            "connect-src 'self'",
        ]
        response['Content-Security-Policy'] = '; '.join(csp)

        return response

    def validate_request(self, request):
        """Validate incoming requests for suspicious patterns"""

        # Check for SQL injection patterns in GET parameters
        sql_patterns = [
            r'union.*select',
            r'insert.*into',
            r'delete.*from',
            r'drop.*table',
            r'update.*set',
        ]

        for key, value in request.GET.items():
            if isinstance(value, str):
                for pattern in sql_patterns:
                    if re.search(pattern, value, re.IGNORECASE):
                        return False

        # Check for XSS patterns
        xss_patterns = [
            r'<script.*?>',
            r'javascript:',
            r'onclick=',
            r'onload=',
        ]

        for key, value in request.GET.items():
            if isinstance(value, str):
                for pattern in xss_patterns:
                    if re.search(pattern, value, re.IGNORECASE):
                        return False

        # Rate limiting check (basic)
        if hasattr(request, 'user') and request.user.is_authenticated:
            # You'd want to implement proper rate limiting with Redis
            # This is just a basic example
            pass

        return True

class ProfileCompletionMiddleware(MiddlewareMixin):
    """
    Middleware to encourage profile completion.
    Because empty profiles are like resumes that just say "hire me".
    """

    def process_request(self, request):
        if (request.user.is_authenticated and 
            not request.path.startswith('/admin/') and
            not request.path.startswith('/static/') and
            not request.path.startswith('/media/')):

            # Check if user has a profile
            if hasattr(request.user, 'profile'):
                profile = request.user.profile
                completeness = profile.calculate_completeness()

                # If profile is less than 50% complete and not already on edit page
                if (completeness < 50 and 
                    not request.path.startswith('/profiles/edit') and
                    not request.path.startswith('/accounts/')):

                    from django.urls import reverse
                    from django.shortcuts import redirect

                    # Store intended destination
                    request.session['redirect_after_profile'] = request.path

                    # Redirect to profile completion
                    return redirect(reverse('profiles:profile_edit'))

        return None

What Our Security Middleware Does:

  1. Security Headers: Protects against clickjacking, XSS, and MIME sniffing attacks.

  2. Input Validation: Checks for SQL injection and XSS patterns in request data.

  3. Profile Completion: Gently nudges users to complete their profiles before using the app fully.

Chapter 20: Settings Configuration - Making It Production Ready

Let's update our settings to be more secure and production-friendly.

# digital_wingman/settings.py (additional settings)

# Security settings for production
if not DEBUG:
    # HTTPS settings
    SECURE_SSL_REDIRECT = True
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True
    SECURE_BROWSER_XSS_FILTER = True
    SECURE_CONTENT_TYPE_NOSNIFF = True

    # HSTS settings
    SECURE_HSTS_SECONDS = 31536000  # 1 year
    SECURE_HSTS_INCLUDE_SUBDOMAINS = True
    SECURE_HSTS_PRELOAD = True

    # Additional security
    SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

# Custom middleware
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',

    # Our custom middleware
    'security.middleware.SecurityMiddleware',
    'security.middleware.ProfileCompletionMiddleware',
]

# Password validation - because 'password' is not a password
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        'OPTIONS': {
            'min_length': 8,
        }
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
    {
        'NAME': 'accounts.validators.CustomPasswordValidator',
    },
]

# Session settings
SESSION_COOKIE_AGE = 1209600  # 2 weeks in seconds
SESSION_SAVE_EVERY_REQUEST = True

# File upload settings
DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880  # 5MB
FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880  # 5MB

# Email settings (for password reset, etc.)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int)
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool)
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
DEFAULT_FROM_EMAIL = 'Digital Wingman <noreply@digitalwingman.com>'

# Logging configuration
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': 'digital_wingman.log',
        },
        'security_file': {
            'level': 'WARNING',
            'class': 'logging.FileHandler',
            'filename': 'security.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'INFO',
            'propagate': True,
        },
        'security': {
            'handlers': ['security_file'],
            'level': 'WARNING',
            'propagate': False,
        },
    },
}

Chapter 21: Custom Password Validator

Let's create a custom password validator that gives better feedback to users.

# accounts/validators.py
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
import re

class CustomPasswordValidator:
    """
    Custom password validator that provides specific, helpful feedback.
    Because "invalid password" is about as helpful as a chocolate teapot.
    """

    def validate(self, password, user=None):
        errors = []

        # Check length
        if len(password) < 8:
            errors.append(_("Password must be at least 8 characters long."))

        # Check for uppercase
        if not re.search(r'[A-Z]', password):
            errors.append(_("Password must contain at least one uppercase letter."))

        # Check for lowercase
        if not re.search(r'[a-z]', password):
            errors.append(_("Password must contain at least one lowercase letter."))

        # Check for numbers
        if not re.search(r'[0-9]', password):
            errors.append(_("Password must contain at least one number."))

        # Check for special characters
        if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
            errors.append(_("Password must contain at least one special character."))

        if errors:
            raise ValidationError(errors)

    def get_help_text(self):
        return _(
            "Your password must contain at least 8 characters, including "
            "uppercase and lowercase letters, numbers, and special characters."
        )

What We've Built in Part 4:

Congratulations! You've just transformed your dating app from "sketchy side project" to "actually legit platform." Here's what you've accomplished:

🛡️ Security Fortress

👤 User Experience Excellence

🎯 Business Logic

🔧 Technical Robustness

Your app now has:

In Part 5, we'll tackle performance optimization, advanced features like photo moderation, and deployment to a real server. But for now, you've built something that's not just functional—it's professional, secure, and user-friendly.

Go ahead—register a test user, complete a profile, and admire your handiwork. You're not just building a dating app anymore; you're building a platform that people can actually trust with their personal information!

Just remember: with great power comes great responsibility. Use your newfound dating app powers for good, not for creepy! 😄

How to Build a Dating Website: Part 5 - Performance, Polish, and Production

Or: From "It Works" to "It Actually Doesn't Suck"

Alright, you glorious code warrior! You've built a secure, functional dating app. But right now, it probably runs like a sloth on tranquilizers when more than 3 people use it. Let's fix that and add some polish that separates the amateurs from the pros.

Chapter 22: Performance Optimization - Making It Zoom

Database Optimization: The Art of Not Waiting Forever

Let's start with database optimizations. Because waiting 5 seconds for profiles to load is a great way to kill the mood.

# profiles/models.py (optimized queries)
from django.db import models
from django.db.models import Prefetch, Count, Q
from django.contrib.auth.models import User

class ProfileManager(models.Manager):
    """
    Custom manager for Profile with optimized queries.
    This is like having a personal assistant who knows exactly what you need.
    """

    def get_optimized_queryset(self):
        """Prefetch related data to avoid N+1 queries"""
        return self.get_queryset().select_related('user').only(
            'user__username',
            'user__first_name', 
            'user__last_name',
            'bio',
            'gender',
            'birth_date',
            'city',
            'country',
            'profile_picture',
            'interests',
            'height_cm',
            'smokes',
            'drinks'
        )

    def get_potential_matches(self, user, limit=20):
        """
        Get potential matches with all the data we need in one query.
        This is the database equivalent of a well-packed suitcase.
        """
        from dating_core.models import Swipe

        # Get users that have been swiped on
        swiped_users = Swipe.objects.filter(
            swiper=user
        ).values_list('swiped_on_id', flat=True)

        # Get profiles with optimized queries
        profiles = self.get_optimized_queryset().exclude(
            user=user
        ).exclude(
            user_id__in=swiped_users
        ).filter(
            gender=user.profile.looking_for
        )[:limit]

        return profiles

    def get_profiles_with_matches(self, user):
        """Get profiles with match count for the discovery page"""
        from dating_core.models import Match

        # Annotate with match count
        return self.get_optimized_queryset().exclude(
            user=user
        ).annotate(
            match_count=Count(
                'user__matches_as_user1',
                filter=Q(user__matches_as_user1__user2=user) |
                       Q(user__matches_as_user2__user1=user)
            )
        ).filter(
            match_count__gt=0
        ).order_by('-match_count')

class Profile(models.Model):
    # ... existing fields ...

    # Add the custom manager
    objects = ProfileManager()

    class Meta:
        indexes = [
            # Index for common search fields
            models.Index(fields=['gender', 'looking_for']),
            models.Index(fields=['city', 'country']),
            models.Index(fields=['birth_date']),
            # Composite index for location-based searches
            models.Index(fields=['city', 'gender', 'looking_for']),
        ]

Why This Database Optimization Matters:

  1. select_related: Fetches related User objects in the same query instead of making separate queries
  2. only(): Only selects the fields we actually need from the database
  3. Indexes: Makes common searches (by location, gender, etc.) lightning fast
  4. Custom Manager: Centralizes our query logic for consistency and performance

Caching: Because Some Things Are Worth Remembering

Let's implement caching so we're not hitting the database for every single request.

# caching/utils.py
from django.core.cache import cache
from django.conf import settings
import pickle
import hashlib

def get_cache_key(prefix, *args, **kwargs):
    """
    Generate a consistent cache key from arguments.
    This is like giving every cached item a unique ID badge.
    """
    key_parts = [prefix] + [str(arg) for arg in args]

    for k, v in sorted(kwargs.items()):
        key_parts.append(f"{k}={v}")

    # Create a hash of the key to ensure it's not too long
    key_string = ":".join(key_parts)
    return f"digital_wingman:{hashlib.md5(key_string.encode()).hexdigest()}"

def cache_profile(profile):
    """
    Cache a profile object for quick access.
    Profiles don't change often, so this is perfect for caching.
    """
    cache_key = get_cache_key('profile', profile.user_id)
    cache.set(cache_key, profile, timeout=3600)  # Cache for 1 hour

def get_cached_profile(user_id):
    """Get a profile from cache if available"""
    cache_key = get_cache_key('profile', user_id)
    return cache.get(cache_key)

def cache_potential_matches(user_id, matches):
    """
    Cache potential matches for a user.
    This prevents re-running expensive queries every time.
    """
    cache_key = get_cache_key('potential_matches', user_id)
    cache.set(cache_key, matches, timeout=900)  # 15 minutes

def get_cached_potential_matches(user_id):
    """Get cached potential matches"""
    cache_key = get_cache_key('potential_matches', user_id)
    return cache.get(cache_key)

Now let's update our views to use caching:

# dating_core/views.py (optimized with caching)
from caching.utils import (
    get_cached_potential_matches, 
    cache_potential_matches,
    get_cached_profile,
    cache_profile
)

@login_required
def dashboard(request):
    """Optimized dashboard with caching"""
    user_profile = get_cached_profile(request.user.id)
    if not user_profile:
        user_profile = get_object_or_404(Profile, user=request.user)
        cache_profile(user_profile)

    # Try to get potential matches from cache first
    potential_matches = get_cached_potential_matches(request.user.id)

    if potential_matches is None:
        # Cache miss - query the database
        potential_matches = Profile.objects.get_potential_matches(
            request.user, 
            limit=12
        )
        cache_potential_matches(request.user.id, list(potential_matches))

    # Calculate profile completeness (cache this too)
    completeness_cache_key = f"completeness_{request.user.id}"
    profile_completeness = cache.get(completeness_cache_key)

    if profile_completeness is None:
        profile_completeness = user_profile.calculate_completeness()
        cache.set(completeness_cache_key, profile_completeness, 3600)  # 1 hour

    # Get recent matches (this is usually a small dataset, so no cache needed)
    user_matches = Match.objects.filter(
        is_active=True
    ).filter(
        models.Q(user1=request.user) | models.Q(user2=request.user)
    ).select_related('user1__profile', 'user2__profile')[:5]

    recent_matches = []
    for match in user_matches:
        other_user = match.user2 if match.user1 == request.user else match.user1
        recent_matches.append(other_user.profile)

    context = {
        'profile': user_profile,
        'potential_matches': potential_matches,
        'recent_matches': recent_matches,
        'profile_completeness': profile_completeness,
        'match_count': user_matches.count(),
    }

    return render(request, 'dating_core/dashboard.html', context)

Chapter 23: Image Optimization - Making Photos Load Fast

Dating apps live and die by photos. Let's make sure they load quickly and look great.

# media/utils.py
from PIL import Image, ImageOps
from io import BytesIO
from django.core.files.uploadedfile import InMemoryUploadedFile
import sys
import os

def optimize_profile_picture(image_file, max_size=(800, 800), quality=85):
    """
    Optimize profile pictures for web display.
    This is like having a personal photo editor for every upload.
    """
    try:
        # Open the image
        img = Image.open(image_file)

        # Convert to RGB if necessary (for PNG with transparency)
        if img.mode in ('RGBA', 'LA', 'P'):
            background = Image.new('RGB', img.size, (255, 255, 255))
            if img.mode == 'P':
                img = img.convert('RGBA')
            background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
            img = background

        # Resize if necessary while maintaining aspect ratio
        img.thumbnail(max_size, Image.Resampling.LANCZOS)

        # Optimize the image
        output = BytesIO()

        # Save as JPEG for better compression
        img.save(output, format='JPEG', quality=quality, optimize=True)

        # Create a new InMemoryUploadedFile
        optimized_file = InMemoryUploadedFile(
            output,
            'ImageField',
            f"{os.path.splitext(image_file.name)[0]}.jpg",
            'image/jpeg',
            sys.getsizeof(output),
            None
        )

        return optimized_file

    except Exception as e:
        # If optimization fails, return the original file
        print(f"Image optimization failed: {e}")
        return image_file

def create_thumbnail(image_file, size=(200, 200)):
    """
    Create a thumbnail version of the image.
    Perfect for profile cards and lists.
    """
    try:
        img = Image.open(image_file)

        # Use ImageOps.thumbnail for better thumbnail creation
        img.thumbnail(size, Image.Resampling.LANCZOS)

        output = BytesIO()
        img.save(output, format='JPEG', quality=70, optimize=True)

        thumbnail_file = InMemoryUploadedFile(
            output,
            'ImageField',
            f"thumb_{os.path.splitext(image_file.name)[0]}.jpg",
            'image/jpeg',
            sys.getsizeof(output),
            None
        )

        return thumbnail_file

    except Exception as e:
        print(f"Thumbnail creation failed: {e}")
        return image_file

Now let's update our profile model to use image optimization:

# profiles/models.py (with image optimization)
class Profile(models.Model):
    # ... existing fields ...

    def save(self, *args, **kwargs):
        """
        Override save method to optimize images before saving.
        This ensures every profile picture is web-optimized.
        """
        # Check if profile_picture is being updated
        if self.profile_picture and hasattr(self.profile_picture, 'file'):
            try:
                # Optimize the main image
                from media.utils import optimize_profile_picture
                self.profile_picture = optimize_profile_picture(
                    self.profile_picture
                )
            except Exception as e:
                # Log the error but don't break the save
                print(f"Error optimizing profile picture: {e}")

        super().save(*args, **kwargs)

        # Clear relevant cache entries
        self.clear_cache()

    def clear_cache(self):
        """Clear cache entries related to this profile"""
        from django.core.cache import cache
        cache.delete(f"profile_{self.user_id}")
        cache.delete(f"completeness_{self.user_id}")
        cache.delete(f"potential_matches_{self.user_id}")

Chapter 24: Advanced Search with Filtering

Let's build a more sophisticated search system that can handle complex queries efficiently.

# search/views.py (advanced version)
from django.db.models import Q, Value, IntegerField
from django.db.models.functions import Coalesce
from django.core.paginator import Paginator
import math

@login_required
def advanced_search(request):
    """
    Advanced search with pagination, scoring, and efficient filtering.
    This is like Tinder's search on steroids.
    """
    # Get search parameters with defaults
    search_params = {
        'gender': request.GET.get('gender', ''),
        'min_age': request.GET.get('min_age', '18'),
        'max_age': request.GET.get('max_age', '99'),
        'city': request.GET.get('city', ''),
        'interests': request.GET.get('interests', ''),
        'has_photo': request.GET.get('has_photo', False),
        'sort_by': request.GET.get('sort_by', 'relevance'),
        'page': request.GET.get('page', 1),
    }

    # Build the base queryset
    profiles = build_search_queryset(request.user, search_params)

    # Apply scoring for relevance
    if search_params['sort_by'] == 'relevance':
        profiles = apply_relevance_scoring(profiles, request.user, search_params)

    # Paginate results
    paginator = Paginator(profiles, 20)  # 20 results per page
    page_obj = paginator.get_page(search_params['page'])

    # Get search statistics
    total_profiles = Profile.objects.exclude(user=request.user).count()
    search_count = profiles.count()

    context = {
        'profiles': page_obj,
        'search_params': search_params,
        'total_profiles': total_profiles,
        'search_count': search_count,
        'page_obj': page_obj,
    }

    return render(request, 'search/advanced_search.html', context)

def build_search_queryset(user, params):
    """
    Build an optimized search queryset based on parameters.
    This is where the search magic happens.
    """
    # Start with all profiles except the current user
    profiles = Profile.objects.get_optimized_queryset().exclude(user=user)

    # Filter by gender preference
    if params['gender']:
        profiles = profiles.filter(gender=params['gender'])
    else:
        # Default to user's preference
        profiles = profiles.filter(gender=user.profile.looking_for)

    # Filter by age range
    from datetime import date, timedelta
    today = date.today()

    if params['min_age']:
        max_birth_date = today - timedelta(days=int(params['min_age'])*365)
        profiles = profiles.filter(birth_date__lte=max_birth_date)

    if params['max_age']:
        min_birth_date = today - timedelta(days=int(params['max_age'])*365)
        profiles = profiles.filter(birth_date__gte=min_birth_date)

    # Filter by location
    if params['city']:
        profiles = profiles.filter(city__icontains=params['city'])

    # Filter by interests
    if params['interests']:
        interest_filters = Q()
        for interest in params['interests'].split(','):
            interest = interest.strip()
            if interest:
                interest_filters |= Q(interests__icontains=interest)
        profiles = profiles.filter(interest_filters)

    # Filter by photo availability
    if params['has_photo']:
        profiles = profiles.exclude(profile_picture='')

    return profiles

def apply_relevance_scoring(profiles, user, params):
    """
    Apply relevance scoring to search results.
    This makes sure the best matches appear first.
    """
    from django.db.models import Case, When, Value, IntegerField

    user_profile = user.profile
    user_interests = set(user_profile.get_interests_list())

    # Annotate with relevance score
    profiles = profiles.annotate(
        # Base score
        base_score=Value(0, output_field=IntegerField()),

        # Location bonus (same city)
        location_bonus=Case(
            When(city=user_profile.city, then=Value(50)),
            default=Value(0),
            output_field=IntegerField()
        ),

        # Photo bonus
        photo_bonus=Case(
            When(profile_picture__isnull=False, then=Value(30)),
            When(profile_picture='', then=Value(0)),
            default=Value(0),
            output_field=IntegerField()
        ),

        # Profile completeness bonus
        completeness_bonus=Case(
            When(
                Q(bio__isnull=False) & 
                Q(bio__gt='') &
                Q(interests__isnull=False) &
                Q(interests__gt=''),
                then=Value(20)
            ),
            default=Value(0),
            output_field=IntegerField()
        )
    )

    # Calculate interest match bonus (this is more complex)
    # We'll do this in Python for simplicity, but you could use database functions
    scored_profiles = []
    for profile in profiles:
        score = 0

        # Add database-calculated scores
        score += getattr(profile, 'location_bonus', 0)
        score += getattr(profile, 'photo_bonus', 0)
        score += getattr(profile, 'completeness_bonus', 0)

        # Calculate interest match
        profile_interests = set(profile.get_interests_list())
        shared_interests = user_interests & profile_interests
        score += len(shared_interests) * 10

        # Age proximity bonus (prefer similar age)
        user_age = user_profile.age()
        profile_age = profile.age()
        if user_age and profile_age:
            age_diff = abs(user_age - profile_age)
            if age_diff <= 2:
                score += 25
            elif age_diff <= 5:
                score += 15
            elif age_diff <= 10:
                score += 5

        scored_profiles.append((profile, score))

    # Sort by score and return profiles
    scored_profiles.sort(key=lambda x: x[1], reverse=True)
    return [profile for profile, score in scored_profiles]

Chapter 25: Real-time Features with WebSockets

Let's add real-time messaging notifications so users know when they get new messages.

First, let's set up Django Channels:

# routing.py
from django.urls import re_path
from messaging import consumers

websocket_urlpatterns = [
    re_path(r'ws/messages/(?P<user_id>\w+)/$', consumers.MessageConsumer.as_asgi()),
]
# messaging/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.contrib.auth.models import User

class MessageConsumer(AsyncWebsocketConsumer):
    """
    WebSocket consumer for real-time messaging notifications.
    This makes messaging feel instant and responsive.
    """

    async def connect(self):
        self.user_id = self.scope['url_route']['kwargs']['user_id']
        self.room_group_name = f'messages_{self.user_id}'

        # Join room group
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )

        await self.accept()

        # Send connection confirmation
        await self.send(text_data=json.dumps({
            'type': 'connection_established',
            'message': 'Connected to messaging service'
        }))

    async def disconnect(self, close_code):
        # Leave room group
        await self.channel_layer.group_del(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message_type = text_data_json['type']

        if message_type == 'typing_indicator':
            # Handle typing indicators
            await self.channel_layer.group_send(
                self.room_group_name,
                {
                    'type': 'typing_indicator',
                    'user_id': text_data_json['user_id'],
                    'is_typing': text_data_json['is_typing']
                }
            )

    # Receive message from room group
    async def chat_message(self, event):
        # Send message to WebSocket
        await self.send(text_data=json.dumps({
            'type': 'new_message',
            'message_id': event['message_id'],
            'conversation_id': event['conversation_id'],
            'sender': event['sender'],
            'content': event['content'],
            'sent_at': event['sent_at']
        }))

    async def typing_indicator(self, event):
        # Send typing indicator to WebSocket
        await self.send(text_data=json.dumps({
            'type': 'typing_indicator',
            'user_id': event['user_id'],
            'is_typing': event['is_typing']
        }))

    @database_sync_to_async
    def get_user(self, user_id):
        return User.objects.get(id=user_id)

Now let's update our messaging views to use WebSockets:

# messaging/views.py (with WebSocket support)
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync

@login_required
def send_message_ajax(request, conversation_id):
    """Enhanced message sending with WebSocket notifications"""
    if request.method == 'POST':
        conversation = get_object_or_404(
            Conversation.objects.filter(
                Q(user1=request.user) | Q(user2=request.user)
            ), 
            id=conversation_id
        )

        data = json.loads(request.body)
        content = data.get('content', '').strip()

        if content:
            message = Message.objects.create(
                conversation=conversation,
                sender=request.user,
                content=content
            )
            conversation.save()

            # Send WebSocket notification to the other user
            other_user = conversation.get_other_user(request.user)
            channel_layer = get_channel_layer()

            async_to_sync(channel_layer.group_send)(
                f'messages_{other_user.id}',
                {
                    'type': 'chat_message',
                    'message_id': message.id,
                    'conversation_id': conversation.id,
                    'sender': request.user.username,
                    'content': content,
                    'sent_at': message.sent_at.isoformat()
                }
            )

            return JsonResponse({
                'success': True,
                'message_id': message.id,
                'sent_at': message.sent_at.isoformat(),
            })

    return JsonResponse({'success': False, 'error': 'Invalid request'})

Chapter 26: Monitoring and Analytics

Let's add some basic analytics to understand how users are interacting with our app.

# analytics/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone

class UserActivity(models.Model):
    """Track user activity for analytics"""
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    activity_type = models.CharField(max_length=50)  # 'login', 'swipe', 'message', etc.
    ip_address = models.GenericIPAddressField(null=True, blank=True)
    user_agent = models.TextField(blank=True)
    timestamp = models.DateTimeField(auto_now_add=True)

    # Additional context
    metadata = models.JSONField(default=dict, blank=True)

    class Meta:
        indexes = [
            models.Index(fields=['user', 'timestamp']),
            models.Index(fields=['activity_type', 'timestamp']),
        ]
        ordering = ['-timestamp']

class SwipeAnalytics(models.Model):
    """Aggregated swipe analytics for performance insights"""
    date = models.DateField()
    total_swipes = models.PositiveIntegerField(default=0)
    right_swipes = models.PositiveIntegerField(default=0)
    left_swipes = models.PositiveIntegerField(default=0)
    super_swipes = models.PositiveIntegerField(default=0)
    matches = models.PositiveIntegerField(default=0)

    class Meta:
        unique_together = ['date']
        ordering = ['-date']

def track_activity(user, activity_type, request=None, **metadata):
    """
    Track user activity for analytics.
    This helps us understand how people actually use the app.
    """
    activity = UserActivity(
        user=user,
        activity_type=activity_type,
        metadata=metadata
    )

    if request:
        activity.ip_address = get_client_ip(request)
        activity.user_agent = request.META.get('HTTP_USER_AGENT', '')

    activity.save()

    # Update daily analytics for swipes
    if activity_type in ['swipe_right', 'swipe_left', 'swipe_super']:
        update_swipe_analytics(activity_type)

def get_client_ip(request):
    """Get client IP address from request"""
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0]
    else:
        ip = request.META.get('REMOTE_ADDR')
    return ip

def update_swipe_analytics(swipe_type):
    """Update daily swipe analytics"""
    today = timezone.now().date()

    analytics, created = SwipeAnalytics.objects.get_or_create(date=today)

    analytics.total_swipes += 1

    if swipe_type == 'swipe_right':
        analytics.right_swipes += 1
    elif swipe_type == 'swipe_left':
        analytics.left_swipes += 1
    elif swipe_type == 'swipe_super':
        analytics.super_swipes += 1

    analytics.save()

Now let's add activity tracking to our key views:

# dating_core/views.py (with analytics)
from analytics.models import track_activity

@login_required
def swipe(request):
    """Enhanced swipe with activity tracking"""
    data = json.loads(request.body)
    user_id = data.get('user_id')
    swipe_type = data.get('swipe_type')

    # Track the swipe activity
    track_activity(
        request.user, 
        f'swipe_{swipe_type}',
        request,
        swiped_user_id=user_id
    )

    # ... rest of swipe logic ...

What We've Built in Part 5:

Boom! You've just transformed your dating app from "functional" to "fantastic." Here's what you've accomplished:

🚀 Performance Excellence

💫 User Experience Polish

📊 Business Intelligence

🔧 Technical Sophistication

Your app now has:

In Part 6, we'll tackle deployment, scaling, and advanced features like AI-powered match suggestions. But for now, you've built something that's not just technically impressive—it's actually delightful to use.

Go ahead—load test it with some simulated users, try the real-time messaging, and watch those optimized images load instantly. You're not just building a dating app anymore; you're building a high-performance platform that can compete with the big players!

Just remember: with great performance comes great responsibility. Use your speedy app powers to bring people together, not just to show off your technical skills! 😄

How to Build a Dating Website: Part 6 - Deployment, Scaling, and AI Magic

Or: From "Localhost Wonder" to "Global Phenomenon"

Alright, you absolute legend! You've built a dating app that's fast, secure, and actually usable. But right now, it's like throwing an amazing party in your basement—only you can attend. Let's get this thing on the internet where it can actually help people find love (or at least a decent date).

Chapter 27: Production Deployment - Making It Internet-Ready

Docker Configuration: The "It Works on My Machine" Solution

Let's start by containerizing our app so it runs consistently everywhere.

# Dockerfile
FROM python:3.11-slim-bullseye

# Set environment variables
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=off \
    PIP_DISABLE_PIP_VERSION_CHECK=on

# Install system dependencies
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    python3-dev \
    && rm -rf /var/lib/apt/lists/*

# Create and set working directory
WORKDIR /app

# Install Python dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt

# Copy project
COPY . .

# Create non-root user for security
RUN useradd --create-home --shell /bin/bash app
USER app

# Collect static files
RUN python manage.py collectstatic --noinput

# Expose port
EXPOSE 8000

# Start server
CMD ["gunicorn", "digital_wingman.wsgi:application", "--bind", "0.0.0.0:8000"]

Now let's create a Docker Compose file for development and production:

# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    command: >
      sh -c "python manage.py migrate &&
             python manage.py runserver 0.0.0.0:8000"
    volumes:
      - .:/app
      - static_volume:/app/static
      - media_volume:/app/media
    ports:
      - "8000:8000"
    environment:
      - DEBUG=True
      - DATABASE_URL=postgres://postgres:password@db:5432/digital_wingman
    depends_on:
      - db
      - redis

  db:
    image: postgres:13
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_DB=digital_wingman
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password

  redis:
    image: redis:6-alpine
    volumes:
      - redis_data:/data

  nginx:
    image: nginx:1.21-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - static_volume:/app/static
      - media_volume:/app/media
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - web

volumes:
  postgres_data:
  redis_data:
  static_volume:
  media_volume:

Production Settings - Locking It Down

Let's create a production-specific settings file:

# digital_wingman/settings_production.py
from .settings import *
import os
import dj_database_url

# Security settings
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com', 'localhost']

# Database
DATABASES = {
    'default': dj_database_url.config(
        default=os.environ.get('DATABASE_URL'),
        conn_max_age=600,
        ssl_require=True
    )
}

# Static files
STATIC_ROOT = '/app/static'
STATIC_URL = '/static/'

# Media files
MEDIA_ROOT = '/app/media'
MEDIA_URL = '/media/'

# Cache
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': os.environ.get('REDIS_URL', 'redis://redis:6379/1'),
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        }
    }
}

# Email configuration
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.sendgrid.net'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'apikey'
EMAIL_HOST_PASSWORD = os.environ.get('SENDGRID_API_KEY')
DEFAULT_FROM_EMAIL = 'Digital Wingman <noreply@digitalwingman.com>'

# Logging
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': '/var/log/digital_wingman.log',
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
        }
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'INFO',
            'propagate': True,
        },
    },
}

# Security middleware
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',  # For static files
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'security.middleware.SecurityMiddleware',
    'security.middleware.ProfileCompletionMiddleware',
]

# WhiteNoise for static files
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

# HTTPS settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000  # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

Nginx Configuration - The Traffic Cop

Let's set up Nginx to handle our web traffic efficiently:

# nginx.conf
events {
    worker_connections 1024;
}

http {
    upstream digital_wingman {
        server web:8000;
    }

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;

    server {
        listen 80;
        server_name yourdomain.com www.yourdomain.com;
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name yourdomain.com www.yourdomain.com;

        ssl_certificate /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;

        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
        ssl_prefer_server_ciphers off;

        # Static files
        location /static/ {
            alias /app/static/;
            expires 1y;
            add_header Cache-Control "public, immutable";
        }

        # Media files
        location /media/ {
            alias /app/media/;
            expires 1w;
            add_header Cache-Control "public";
        }

        # API rate limiting
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://digital_wingman;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_redirect off;
        }

        # Auth rate limiting
        location /accounts/ {
            limit_req zone=auth burst=5 nodelay;
            proxy_pass http://digital_wingman;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_redirect off;
        }

        # Main application
        location / {
            proxy_pass http://digital_wingman;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_redirect off;
        }

        # Security headers
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header Referrer-Policy "no-referrer-when-downgrade" always;
        add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
    }
}

Chapter 28: AI-Powered Match Suggestions

Let's add some machine learning magic to suggest better matches. Because sometimes algorithms know what you want better than you do.

# matching/ml_matcher.py
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.cluster import KMeans
import pandas as pd
from django.db.models import Q
import joblib
import os

class MLMatchRecommender:
    """
    Machine learning-powered match recommendation system.
    This is like having a digital cupid that actually understands people.
    """

    def __init__(self):
        self.vectorizer = TfidfVectorizer(
            max_features=1000,
            stop_words='english',
            ngram_range=(1, 2)
        )
        self.kmeans = KMeans(n_clusters=5, random_state=42)
        self.is_fitted = False

    def prepare_profile_data(self, profiles):
        """
        Prepare profile data for machine learning.
        Turns messy human data into clean numbers.
        """
        data = []

        for profile in profiles:
            # Combine text features
            text_features = []

            if profile.bio:
                text_features.append(profile.bio)

            if profile.interests:
                text_features.append(profile.interests)

            if profile.city:
                text_features.append(profile.city)

            text_data = ' '.join(text_features)

            # Numerical features
            age = profile.age() or 30
            has_photo = 1 if profile.profile_picture else 0

            data.append({
                'id': profile.user_id,
                'text': text_data,
                'age': age,
                'has_photo': has_photo,
                'gender': profile.gender,
                'looking_for': profile.looking_for
            })

        return pd.DataFrame(data)

    def fit(self, profiles):
        """
        Train the ML model on existing profiles.
        This is where the magic learning happens.
        """
        if len(profiles) < 10:
            # Not enough data to train properly
            return

        df = self.prepare_profile_data(profiles)

        # Fit TF-IDF vectorizer on text data
        if not df['text'].empty:
            text_vectors = self.vectorizer.fit_transform(df['text'])

            # Combine with numerical features
            numerical_features = df[['age', 'has_photo']].values
            all_features = np.hstack([text_vectors.toarray(), numerical_features])

            # Fit K-means clustering
            self.kmeans.fit(all_features)
            self.is_fitted = True

            # Save the model
            self.save_model()

    def get_recommendations(self, user_profile, potential_profiles, top_n=10):
        """
        Get ML-powered recommendations for a user.
        This suggests matches based on actual compatibility, not just looks.
        """
        if not self.is_fitted or len(potential_profiles) < 5:
            # Fall back to basic recommendations if ML isn't ready
            return self.get_basic_recommendations(user_profile, potential_profiles, top_n)

        # Prepare data for prediction
        all_profiles = [user_profile] + list(potential_profiles)
        df = self.prepare_profile_data(all_profiles)

        # Transform features
        text_vectors = self.vectorizer.transform(df['text'])
        numerical_features = df[['age', 'has_photo']].values
        all_features = np.hstack([text_vectors.toarray(), numerical_features])

        # Get cluster assignments
        clusters = self.kmeans.predict(all_features)
        user_cluster = clusters[0]  # First profile is the user

        # Find profiles in the same cluster (similar people)
        similar_indices = np.where(clusters[1:] == user_cluster)[0]

        if len(similar_indices) == 0:
            # No similar profiles found, fall back to basic
            return self.get_basic_recommendations(user_profile, potential_profiles, top_n)

        # Calculate similarity scores
        user_vector = all_features[0:1]
        similar_vectors = all_features[1:][similar_indices]

        similarities = cosine_similarity(user_vector, similar_vectors)[0]

        # Sort by similarity score
        scored_indices = list(zip(similar_indices, similarities))
        scored_indices.sort(key=lambda x: x[1], reverse=True)

        # Get top recommendations
        top_indices = [idx for idx, score in scored_indices[:top_n]]
        recommendations = [potential_profiles[i] for i in top_indices]

        return recommendations

    def get_basic_recommendations(self, user_profile, potential_profiles, top_n=10):
        """
        Fallback recommendation system when ML isn't available.
        Still smarter than random chance.
        """
        scored_profiles = []
        user_interests = set(user_profile.get_interests_list())

        for profile in potential_profiles:
            score = 0

            # Interest matching
            profile_interests = set(profile.get_interests_list())
            shared_interests = user_interests & profile_interests
            score += len(shared_interests) * 10

            # Age proximity
            user_age = user_profile.age()
            profile_age = profile.age()
            if user_age and profile_age:
                age_diff = abs(user_age - profile_age)
                if age_diff <= 5:
                    score += 20
                elif age_diff <= 10:
                    score += 10

            # Location bonus
            if user_profile.city and profile.city and user_profile.city == profile.city:
                score += 15

            # Profile completeness bonus
            if profile.bio and len(profile.bio) > 50:
                score += 5
            if profile.profile_picture:
                score += 10

            scored_profiles.append((profile, score))

        # Sort by score and return top profiles
        scored_profiles.sort(key=lambda x: x[1], reverse=True)
        return [profile for profile, score in scored_profiles[:top_n]]

    def save_model(self):
        """Save the trained model to disk"""
        model_dir = 'ml_models'
        os.makedirs(model_dir, exist_ok=True)

        joblib.dump(self.vectorizer, os.path.join(model_dir, 'vectorizer.joblib'))
        joblib.dump(self.kmeans, os.path.join(model_dir, 'kmeans.joblib'))

    def load_model(self):
        """Load a trained model from disk"""
        try:
            self.vectorizer = joblib.load('ml_models/vectorizer.joblib')
            self.kmeans = joblib.load('ml_models/kmeans.joblib')
            self.is_fitted = True
        except FileNotFoundError:
            self.is_fitted = False

Now let's integrate the ML recommender into our views:

# dating_core/views.py (with ML recommendations)
from matching.ml_matcher import MLMatchRecommender
from django.core.cache import cache

@login_required
def smart_dashboard(request):
    """
    Enhanced dashboard with ML-powered recommendations.
    This is where AI meets romance.
    """
    user_profile = get_object_or_404(Profile, user=request.user)

    # Try to get ML recommendations from cache
    cache_key = f"ml_recommendations_{request.user.id}"
    potential_matches = cache.get(cache_key)

    if potential_matches is None:
        # Get base potential matches
        base_matches = Profile.objects.get_potential_matches(request.user, limit=50)

        if len(base_matches) > 10:
            # Use ML recommender
            recommender = MLMatchRecommender()
            recommender.load_model()  # Try to load existing model

            # If no model exists, train on all profiles
            if not recommender.is_fitted:
                all_profiles = Profile.objects.get_optimized_queryset().exclude(user=request.user)
                recommender.fit(all_profiles)

            potential_matches = recommender.get_recommendations(
                user_profile, base_matches, top_n=12
            )
        else:
            potential_matches = base_matches

        # Cache for 15 minutes
        cache.set(cache_key, list(potential_matches), 900)

    # Rest of the dashboard logic remains the same...
    context = {
        'profile': user_profile,
        'potential_matches': potential_matches,
        # ... other context
    }

    return render(request, 'dating_core/dashboard.html', context)

Chapter 29: Background Tasks with Celery

Let's set up background tasks for expensive operations like ML training and email notifications.

# digital_wingman/celery.py
import os
from celery import Celery

# Set the default Django settings module
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'digital_wingman.settings')

app = Celery('digital_wingman')

# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
app.config_from_object('django.conf:settings', namespace='CELERY')

# Load task modules from all registered Django app configs.
app.autodiscover_tasks()

@app.task(bind=True)
def debug_task(self):
    print(f'Request: {self.request!r}')
# tasks.py
from celery import shared_task
from django.core.mail import send_mail
from matching.ml_matcher import MLMatchRecommender
from profiles.models import Profile
from analytics.models import SwipeAnalytics
import pandas as pd
from django.db.models import Count
from datetime import datetime, timedelta

@shared_task
def train_ml_model():
    """
    Background task to train the ML model on all profiles.
    This runs overnight when traffic is low.
    """
    try:
        profiles = Profile.objects.get_optimized_queryset()

        if len(profiles) >= 100:  # Only train if we have enough data
            recommender = MLMatchRecommender()
            recommender.fit(profiles)

            return f"ML model trained on {len(profiles)} profiles"
        else:
            return "Not enough profiles to train ML model"

    except Exception as e:
        return f"ML training failed: {str(e)}"

@shared_task
def send_match_notification(user_email, match_name, match_profile_url):
    """
    Send email notification when users match.
    Because everyone loves that "It's a Match!" feeling.
    """
    subject = "🎉 It's a Match! You and {match_name} like each other"
    message = f"""
    Hey there!

    Great news! You and {match_name} have matched on Digital Wingman.

    This means you're both interested in each other. Why not send them a message?

    View their profile: {match_profile_url}

    Happy dating!
    The Digital Wingman Team

    P.S. Don't wait too long - make the first move!
    """

    send_mail(
        subject,
        message,
        'matches@digitalwingman.com',
        [user_email],
        fail_silently=False,
    )

@shared_task
def generate_daily_analytics_report():
    """
    Generate daily analytics report for admin insights.
    This helps understand how the platform is performing.
    """
    yesterday = datetime.now() - timedelta(days=1)

    # Get yesterday's analytics
    analytics = SwipeAnalytics.objects.filter(date=yesterday.date()).first()

    if analytics:
        # Calculate key metrics
        match_rate = (analytics.matches / analytics.total_swipes * 100) if analytics.total_swipes > 0 else 0
        right_swipe_rate = (analytics.right_swipes / analytics.total_swipes * 100) if analytics.total_swipes > 0 else 0

        report = f"""
        Digital Wingman Daily Report - {yesterday.date()}

        📊 Activity Summary:
        - Total Swipes: {analytics.total_swipes}
        - Right Swipes: {analytics.right_swipes}
        - Left Swipes: {analytics.left_swipes}
        - Super Swipes: {analytics.super_swipes}
        - Matches: {analytics.matches}

        📈 Key Metrics:
        - Match Rate: {match_rate:.1f}%
        - Right Swipe Rate: {right_swipe_rate:.1f}%

        💡 Insights:
        {get_insights_from_analytics(analytics)}
        """

        # Send to admin email
        send_mail(
            f"Daily Analytics Report - {yesterday.date()}",
            report,
            'analytics@digitalwingman.com',
            ['admin@digitalwingman.com'],
            fail_silently=False,
        )

def get_insights_from_analytics(analytics):
    """Generate insights from analytics data"""
    insights = []

    match_rate = (analytics.matches / analytics.total_swipes * 100) if analytics.total_swipes > 0 else 0

    if match_rate > 10:
        insights.append("🔥 High match rate today! Users are finding great connections.")
    elif match_rate < 5:
        insights.append("📉 Match rate is lower than usual. Consider optimizing matching algorithm.")

    if analytics.super_swipes > analytics.total_swipes * 0.1:
        insights.append("⭐ Users are loving the Super Swipe feature!")

    return "\n".join(insights) if insights else "No significant insights today."

Chapter 30: Advanced Features - Making It Stand Out

Let's add some killer features that make our dating app truly special.

Icebreaker Suggestions

# messaging/icebreakers.py
import random

class IcebreakerGenerator:
    """
    Generate smart icebreaker messages based on profile data.
    Because "hey" is boring and "u up?" is just sad.
    """

    INTEREST_BASED = [
        "I see you're into {interest}. What's your favorite thing about it?",
        "Wow, {interest}! That's awesome. Tell me more about that?",
        "I've always wanted to learn more about {interest}. Any tips for a beginner?",
        "Your {interest} passion is really inspiring! How did you get into it?",
    ]

    BIO_BASED = [
        "I loved your bio, especially the part about {topic}. That's so interesting!",
        "Your bio really stood out to me. I'm curious about {topic}.",
        "I noticed in your bio you mentioned {topic}. That's really cool!",
    ]

    TRAVEL_BASED = [
        "I see you're from {city}. What's the best thing about living there?",
        "Your photos from {city} look amazing! What's your favorite spot?",
        "I've always wanted to visit {city}. Any recommendations for when I go?",
    ]

    @staticmethod
    def generate_icebreaker(profile):
        """
        Generate a personalized icebreaker based on profile data.
        This helps users start conversations that actually go somewhere.
        """
        icebreakers = []

        # Interest-based icebreakers
        interests = profile.get_interests_list()
        if interests:
            interest = random.choice(interests)
            template = random.choice(IcebreakerGenerator.INTEREST_BASED)
            icebreakers.append(template.format(interest=interest))

        # Bio-based icebreakers
        if profile.bio and len(profile.bio) > 50:
            # Extract a topic from bio (simplified)
            words = profile.bio.split()[:10]  # First 10 words
            topic = ' '.join(words) + '...'
            template = random.choice(IcebreakerGenerator.BIO_BASED)
            icebreakers.append(template.format(topic=topic))

        # Location-based icebreakers
        if profile.city:
            template = random.choice(IcebreakerGenerator.TRAVEL_BASED)
            icebreakers.append(template.format(city=profile.city))

        # Fallback icebreakers
        fallbacks = [
            "Your profile really caught my eye! What's been the best part of your week?",
            "I'd love to get to know you better. What are you passionate about?",
            "You seem really interesting! What's something you're excited about right now?",
            "I had to message you because your smile is contagious! What's making you happy today?",
        ]

        icebreakers.extend(fallbacks)

        return random.choice(icebreakers)

Photo Verification System

# verification/models.py
from django.db import models
from django.contrib.auth.models import User
from django.core.files.storage import FileSystemStorage

class PhotoVerification(models.Model):
    """
    Photo verification system to ensure users are who they say they are.
    This builds trust and reduces catfishing.
    """
    VERIFICATION_STATUS = [
        ('pending', 'Pending Review'),
        ('approved', 'Verified'),
        ('rejected', 'Rejected'),
        ('expired', 'Expired'),
    ]

    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='verifications')
    verification_photo = models.ImageField(
        upload_to='verification_photos/',
        help_text="Photo holding a paper with your username and today's date"
    )
    status = models.CharField(max_length=20, choices=VERIFICATION_STATUS, default='pending')
    submitted_at = models.DateTimeField(auto_now_add=True)
    reviewed_at = models.DateTimeField(null=True, blank=True)
    reviewed_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='reviews')
    rejection_reason = models.TextField(blank=True)

    class Meta:
        ordering = ['-submitted_at']

    def __str__(self):
        return f"Verification for {self.user.username} - {self.status}"

    def is_expired(self):
        """Check if verification submission is expired (older than 24 hours)"""
        from django.utils import timezone
        return (timezone.now() - self.submitted_at).days > 1

    def approve(self, reviewed_by):
        """Approve the verification"""
        self.status = 'approved'
        self.reviewed_at = timezone.now()
        self.reviewed_by = reviewed_by
        self.save()

        # Update user profile
        self.user.profile.is_verified = True
        self.user.profile.save()

    def reject(self, reviewed_by, reason):
        """Reject the verification with a reason"""
        self.status = 'rejected'
        self.reviewed_at = timezone.now()
        self.reviewed_by = reviewed_by
        self.rejection_reason = reason
        self.save()

@receiver(models.signals.post_save, sender=PhotoVerification)
def update_verification_status(sender, instance, **kwargs):
    """Update verification status if expired"""
    if instance.status == 'pending' and instance.is_expired():
        instance.status = 'expired'
        instance.save()

What We've Built in Part 6:

Congratulations! You've just transformed your dating app from "cool side project" to "legitimate business-ready platform." Here's what you've accomplished:

🚀 Production Deployment

🤖 AI-Powered Intelligence

Advanced Features

🏗️ Scalable Architecture

Your app now has:

🎯 Ready for Launch Checklist:

  1. Infrastructure

    • Docker containers
    • Nginx with SSL
    • Database and Redis
    • Celery workers
  2. Intelligence

    • ML recommendations
    • Smart icebreakers
    • Analytics insights
  3. Trust & Safety

    • Photo verification
    • Rate limiting
    • Content moderation tools
  4. User Experience

    • Real-time features
    • Background processing
    • Professional design
  5. Monitoring

    • Error tracking
    • Performance monitoring
    • Usage analytics

You've built a dating platform that's not just technically impressive—it's business-ready, scalable, and packed with features that actual dating apps charge money for.

The next step? Actually deploying it and watching real people find love through code you wrote. How cool is that?

Just remember: with great app-building power comes great responsibility. Use your skills to create positive experiences and maybe, just maybe, help create some happy relationships along the way! ❤️

Now go forth and deploy! The world needs your digital cupid skills.

Back to ChameleonSoftwareOnline.com