django python

How to make a website with python and django

Welcome to this comprehensive guide on building modern websites using Django 5.0 and Python 3.12. This tutorial will walk you through everything you need to know to create robust, scalable web applications using the latest features and best practices. Whether you’re a beginner or an experienced developer, this guide will help you understand the fundamentals and advanced concepts of Django web development.

Setting Up Your Development Environment

A proper development environment is crucial for efficient Django development. We’ll use Python 3.12 for its improved performance and new features like better error messages, improved type hints, and faster startup times.

Installing Python 3.12 and Django 5.0

First, download Python 3.12 from the official Python website. After installation, verify your Python version:

python --version
# Should output: Python 3.12.x

It’s essential to use virtual environments to isolate project dependencies. Here’s how to set one up:

# On Windows
python -m venv mywebsite_env
mywebsite_env\Scripts\activate

# On macOS/Linux
python3 -m venv mywebsite_env
source mywebsite_env/bin/activate

Install Django 5.0 and other essential packages:

pip install django==5.0
pip install python-dotenv
pip install django-debug-toolbar
pip install django-compressor
pip install pillow
pip install django-allauth
pip install django-crispy-forms

Create a requirements.txt file:

pip freeze > requirements.txt

Development Tools Setup

For optimal development, install these VS Code extensions:

  • Python
  • Django
  • Pylance
  • Git Lens
  • Python Test Explorer

Configure VS Code settings.json for Django:

{
    "python.linting.pylintEnabled": true,
    "python.linting.enabled": true,
    "python.formatting.provider": "black",
    "editor.formatOnSave": true,
    "files.associations": {
        "**/*.html": "html",
        "**/templates/**/*.html": "django-html",
        "**/templates/**/*": "django-txt"
    }
}

Django Project Structure and Best Practices 2024

Creating a well-organized project structure is crucial for maintainability. Let’s create a new Django project with a modern structure:

django-admin startproject mywebsite
cd mywebsite
python manage.py startapp core
python manage.py startapp accounts
python manage.py startapp api

Advanced Project Structure:

mywebsite/
├── mywebsite/
│   ├── __init__.py
│   ├── settings/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── development.py
│   │   ├── production.py
│   │   └── staging.py
│   ├── urls.py
│   ├── asgi.py
│   └── wsgi.py
├── apps/
│   ├── core/
│   ├── accounts/
│   └── api/
├── templates/
│   ├── base.html
│   ├── components/
│   └── pages/
├── static/
│   ├── css/
│   ├── js/
│   └── images/
├── media/
├── docs/
├── tests/
│   ├── integration/
│   └── unit/
├── requirements/
│   ├── base.txt
│   ├── development.txt
│   └── production.txt
├── scripts/
│   ├── deploy.sh
│   └── setup.sh
├── .env.example
├── .gitignore
├── README.md
└── manage.py

Comprehensive Settings Configuration

Create a modular settings structure (in settings/base.py):

from pathlib import Path
from dotenv import load_dotenv
import os

# Load environment variables
load_dotenv()

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

SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')

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

    # Third-party apps
    'debug_toolbar',
    'compressor',
    'allauth',
    'allauth.account',
    'crispy_forms',
    'rest_framework',

    # Local apps
    'apps.core.apps.CoreConfig',
    'apps.accounts.apps.AccountsConfig',
    'apps.api.apps.ApiConfig',
]

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',
    'debug_toolbar.middleware.DebugToolbarMiddleware',
]

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 configuration with environment variables
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.getenv('DB_NAME'),
        'USER': os.getenv('DB_USER'),
        'PASSWORD': os.getenv('DB_PASSWORD'),
        'HOST': os.getenv('DB_HOST', 'localhost'),
        'PORT': os.getenv('DB_PORT', '5432'),
    }
}

Leveraging Django’s Latest Features

Advanced Async Views

Django 5.0 brings improved async support. Here’s a comprehensive example:

# views.py
import asyncio
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.core.cache import cache
from typing import Dict, Any

async def fetch_external_api_data(url: str) -> Dict[str, Any]:
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        return response.json()

async def get_cached_data(key: str) -> Dict[str, Any]:
    if cached_data := await cache.aget(key):
        return cached_data
    return None

@require_http_methods(["GET"])
async def async_dashboard(request):
    # Parallel async operations
    user_data = asyncio.create_task(get_user_data(request.user.id))
    analytics = asyncio.create_task(fetch_external_api_data(ANALYTICS_API_URL))
    notifications = asyncio.create_task(get_user_notifications(request.user.id))

    # Wait for all tasks to complete
    dashboard_data = await asyncio.gather(
        user_data,
        analytics,
        notifications
    )

    return JsonResponse({
        'user_data': dashboard_data[0],
        'analytics': dashboard_data[1],
        'notifications': dashboard_data[2]
    })

Advanced HTMX Integration

Create a more sophisticated HTMX implementation:

<!-- 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 %}{% endblock %}</title>
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
    <script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
    {% compress css %}
    <link href="{% static 'css/main.css' %}" rel="stylesheet">
    {% endcompress %}
</head>
<body hx-boost="true">
    {% include 'components/navbar.html' %}

    <div class="container mx-auto px-4">
        {% if messages %}
            {% for message in messages %}
                <div class="alert alert-{{ message.tags }}"
                     hx-swap-oob="true"
                     _="on load wait 5s then add .fade-out then wait 0.5s then remove me">
                    {{ message }}
                </div>
            {% endfor %}
        {% endif %}

        {% block content %}
        {% endblock %}
    </div>

    {% compress js %}
    <script src="{% static 'js/main.js' %}"></script>
    {% endcompress %}
</body>
</html>

Example of an advanced HTMX-powered infinite scroll with search:

# views.py
from django.core.paginator import Paginator
from django.template.loader import render_to_string
from django.http import HttpResponse

class ArticleListView(View):
    template_name = 'articles/list.html'
    partial_template_name = 'articles/partials/article_list.html'

    def get(self, request):
        search_query = request.GET.get('q', '')
        page_number = request.GET.get('page', 1)

        articles = Article.objects.filter(
            Q(title__icontains=search_query) |
            Q(content__icontains=search_query)
        ).select_related('author').prefetch_related('tags')

        paginator = Paginator(articles, 10)
        page_obj = paginator.get_page(page_number)

        context = {
            'page_obj': page_obj,
            'search_query': search_query
        }

        if request.htmx:
            html = render_to_string(
                self.partial_template_name,
                context,
                request=request
            )
            return HttpResponse(html)

        return render(request, self.template_name, context)
<!-- templates/articles/list.html -->
<div class="space-y-4">
    <input type="text"
           name="search"
           hx-get="{% url 'article-list' %}"
           hx-trigger="keyup changed delay:500ms"
           hx-target="#article-list"
           hx-push-url="true"
           placeholder="Search articles..."
           value="{{ search_query }}">

    <div id="article-list"
         hx-trigger="revealed"
         hx-get="{% url 'article-list' %}?page={{ page_obj.next_page_number }}"
         hx-target="this"
         hx-swap="beforeend">
        {% include 'articles/partials/article_list.html' %}
    </div>
</div>

Modern Frontend Integration

Advanced Tailwind CSS Configuration

Create a sophisticated Tailwind setup with custom configuration:

// tailwind.config.js
const colors = require('tailwindcss/colors')

module.exports = {
  content: [
    "./templates/**/*.html",
    "./apps/*/templates/**/*.html",
  ],
  theme: {
    extend: {
      colors: {
        primary: colors.blue[600],
        secondary: colors.gray[600],
        success: colors.green[500],
        danger: colors.red[500],
        warning: colors.yellow[500],
      },
      fontFamily: {
        sans: ['Inter var', ...defaultTheme.fontFamily.sans],
      },
      spacing: {
        '128': '32rem',
        '144': '36rem',
      },
      borderRadius: {
        '4xl': '2rem',
      }
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
    require('@tailwindcss/aspect-ratio'),
  ],
}

Database Design and Advanced Models

Example of sophisticated Django models with type hints and advanced features:

# models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils.text import slugify
from typing import Any, List
import uuid

class BaseModel(models.Model):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

class Tag(BaseModel):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(unique=True)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)

    def __str__(self) -> str:
        return self.name

class Article(BaseModel):
    class Status(models.TextChoices):
        DRAFT = 'DR', 'Draft'
        PUBLISHED = 'PB', 'Published'
        ARCHIVED = 'AR', 'Archived'

    title: str = models.CharField(max_length=200)
    slug: str = models.SlugField(unique=True)
    content: str = models.TextField()
    author: Any = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='articles'
    )
    tags: List[Tag] = models.ManyToManyField(Tag, related_name='articles')
    status = models.CharField(
        max_length=2,
        choices=Status.choices,
        default=Status.DRAFT
    )
    featured_image = models.ImageField(
        upload_to='articles/%Y/%m/',
        null=True,
        blank=True
    )
    read_time = models.PositiveIntegerField(default=0)

    class Meta:
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['status', 'created_at']),
            models.Index(fields=['author', 'status']),
        ]

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        if not self.read_time and self.content:
            self.read_time = self.calculate_read_time()
        super().save(*args, **kwargs)

    def calculate_read_time(self) -> int:
        words_per_minute = 200
        word_count = len(self.content.split())
        return max(1, round(word_count / words_per_minute))

    def get_absolute_url(self) -> str:
        return reverse('article-detail', kwargs={'slug': self.slug})

Authentication and Advanced User Management

Implementation of a sophisticated custom user model with additional features:

# users/models.py (continued)
class CustomUser(AbstractUser):
    class Role(models.TextChoices):
        ADMIN = 'AD', _('Admin')
        EDITOR = 'ED', _('Editor')
        AUTHOR = 'AU', _('Author')
        SUBSCRIBER = 'SU', _('Subscriber')

    email = models.EmailField(_('email address'), unique=True)
    role = models.CharField(
        max_length=2,
        choices=Role.choices,
        default=Role.SUBSCRIBER
    )
    bio = models.TextField(max_length=500, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
    website = models.URLField(max_length=200, blank=True)
    location = models.CharField(max_length=100, blank=True)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    def get_full_name(self) -> str:
        return f"{self.first_name} {self.last_name}"

    def get_role_display_name(self) -> str:
        return self.get_role_display()

    @property
    def is_admin(self) -> bool:
        return self.role == self.Role.ADMIN

Advanced Authentication Views

# accounts/views.py
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import UpdateView
from django.urls import reverse_lazy
from .forms import CustomUserChangeForm

User = get_user_model()

class ProfileUpdateView(LoginRequiredMixin, UpdateView):
    model = User
    form_class = CustomUserChangeForm
    template_name = 'accounts/profile_update.html'
    success_url = reverse_lazy('profile')

    def get_object(self, queryset=None):
        return self.request.user

    def form_valid(self, form):
        response = super().form_valid(form)
        messages.success(self.request, 'Profile updated successfully!')
        return response

Advanced API Development

Create a robust REST API using Django REST Framework:

# api/serializers.py
from rest_framework import serializers
from core.models import Article, Tag

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ['id', 'name', 'slug']

class ArticleSerializer(serializers.ModelSerializer):
    author = serializers.StringRelatedField()
    tags = TagSerializer(many=True, read_only=True)
    status_display = serializers.CharField(
        source='get_status_display',
        read_only=True
    )

    class Meta:
        model = Article
        fields = [
            'id', 'title', 'slug', 'content', 'author',
            'tags', 'status', 'status_display', 'featured_image',
            'read_time', 'created_at', 'updated_at'
        ]
        read_only_fields = ['slug', 'read_time']

class ArticleCreateSerializer(serializers.ModelSerializer):
    tags = serializers.ListField(
        child=serializers.CharField(),
        write_only=True,
        required=False
    )

    class Meta:
        model = Article
        fields = ['title', 'content', 'tags', 'status', 'featured_image']

    def create(self, validated_data):
        tags_data = validated_data.pop('tags', [])
        article = Article.objects.create(**validated_data)

        for tag_name in tags_data:
            tag, _ = Tag.objects.get_or_create(name=tag_name)
            article.tags.add(tag)

        return article

API Views with Advanced Features

# api/views.py
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from .serializers import ArticleSerializer, ArticleCreateSerializer
from core.models import Article

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter
    ]
    filterset_fields = ['status', 'author', 'tags']
    search_fields = ['title', 'content']
    ordering_fields = ['created_at', 'updated_at', 'read_time']
    permission_classes = [IsAuthenticated]

    def get_serializer_class(self):
        if self.action == 'create':
            return ArticleCreateSerializer
        return ArticleSerializer

    def get_queryset(self):
        queryset = super().get_queryset()
        if self.action == 'list':
            return queryset.select_related('author')\
                         .prefetch_related('tags')
        return queryset

    @action(detail=True, methods=['post'])
    def publish(self, request, pk=None):
        article = self.get_object()
        article.status = Article.Status.PUBLISHED
        article.save()
        return Response(
            {'status': 'published'},
            status=status.HTTP_200_OK
        )

Comprehensive Testing Strategy

# tests/test_models.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from core.models import Article, Tag
from datetime import date

User = get_user_model()

class ArticleTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass123'
        )
        cls.tag = Tag.objects.create(name='Python')
        cls.article = Article.objects.create(
            title='Test Article',
            content='Test Content' * 100,  # Create substantial content
            author=cls.user,
            status=Article.Status.DRAFT
        )
        cls.article.tags.add(cls.tag)

    def test_article_creation(self):
        self.assertEqual(self.article.title, 'Test Article')
        self.assertEqual(self.article.author, self.user)
        self.assertTrue(self.article.slug)

    def test_read_time_calculation(self):
        self.assertTrue(self.article.read_time > 0)

    def test_article_str_representation(self):
        self.assertEqual(str(self.article), self.article.title)

    def test_article_absolute_url(self):
        url = self.article.get_absolute_url()
        self.assertEqual(url, f'/articles/{self.article.slug}/')

# tests/test_views.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model
import json

User = get_user_model()

class ArticleAPITests(TestCase):
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass123'
        )
        self.client.login(
            email='test@example.com',
            password='testpass123'
        )

    def test_create_article(self):
        url = reverse('api:article-list')
        data = {
            'title': 'New Article',
            'content': 'Article content',
            'tags': ['Python', 'Django'],
            'status': 'DR'
        }
        response = self.client.post(
            url,
            data=json.dumps(data),
            content_type='application/json'
        )
        self.assertEqual(response.status_code, 201)
        self.assertEqual(Article.objects.count(), 1)

Deployment Configuration

Create a production-ready deployment setup:

# settings/production.py
from .base import *

DEBUG = False

ALLOWED_HOSTS = [
    'yourdomain.com',
    'www.yourdomain.com',
]

# Security settings
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

# Static and media files
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_ROOT = BASE_DIR / 'mediafiles'

# Cache settings
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': os.getenv('REDIS_URL'),
    }
}

# Email configuration
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.getenv('EMAIL_HOST')
EMAIL_PORT = int(os.getenv('EMAIL_PORT', 587))
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')

# Logging configuration
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': BASE_DIR / 'logs' / 'django.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'ERROR',
            'propagate': True,
        },
    },
}

Gunicorn Configuration

# gunicorn.conf.py
import multiprocessing

bind = "unix:/run/gunicorn.sock"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "uvicorn.workers.UvicornWorker"
max_requests = 1000
max_requests_jitter = 50
timeout = 30
keep_alive = 2

# Logging
accesslog = "-"
errorlog = "-"
loglevel = "info"

# Process naming
proc_name = "mywebsite"

References and Further Reading

  1. Django Official Documentation
  2. Django REST Framework
  3. HTMX Documentation
  4. Tailwind CSS Documentation
  5. Python 3.12 Release Notes
  6. Django 5.0 Release Notes
  7. Django Best Practices
  8. Full Stack Python
  9. Test-Driven Development with Python
  10. Two Scoops of Django
See also  Building Secure APIs with FastAPI: A Best Practices Guide

Remember to always check the official documentation for the most up-to-date information and best practices. This guide covers the fundamentals and advanced concepts, but Django and Python are constantly evolving with new features and improvements.

ayush.mandal11@gmail.com
ayush.mandal11@gmail.com
Articles: 26

Leave a Reply

Your email address will not be published. Required fields are marked *