Skip to content

vuanhtuan1012/django-web-framework

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

46 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Django Web Framework

This repository is a structured, comprehensive collection of notes designed based on the Django Web Framework offered on Coursera. It brings together essential web fundamentals, Python best practices, and Django's core features in a clear, organized format that's easy to study and reference.

The material progresses from foundational concepts, such as HTTP, virtual environments, and project architecture to more advanced topics including ORM modeling, class‑based views, URL routing, templates, forms, security, and testing. Each section is crafted to help you build a strong mental model of how Django applications work, both internally and in real-world development.

To bridge theory and practice, the repository also includes an applied project, Little Lemon, demonstrating how these concepts come together in a functional Django application. Whether you're learning Django for the first time or refining your understanding, this repository aims to be a reliable companion throughout your journey.

To provide a quick overview of the project in action, a short demo video is available. It walks through the main pages, demonstrates the reservation workflow, and highlights the responsive design built with Tailwind CSS, Alpine.js, and Heroicons.

▶ Watch demo video

Introduction to Django

Hyper Text Markup Language (HTML) vs. HTML5

  • HTML (Hyper Text Markup Language) is the standard language for structuring web pages.
  • HTML5 is the newest version of HTML, adding modern features for today's web.
  • Key differences:
    • HTML5 introduced semantic tags such as <header>, <footer>, <nav>, <section>, <article>, <aside>, so browsers and developers understand page structure better, improve code readability. Since these tags have built-in meaning, they tell the browser (and developers, screen renders, search engines) what the content represents.
      • <header> -> top of a page or section.
      • <nav> -> navigation menu.
      • <article> -> self-contained content (blog post, news article, post).
      • <section> -> logical grouping of related content.
      • <aside> -> sidebar or related info.
      • <footer> -> bottom of a page or section.
    • HTML uses mostly <div> for layout. It has no meaning by default.
      • is just a generic container.
      • used mainly for grouping elements for styling or scripting.
      • does not describe the purpose of its content.
    • HTML5 introduces new form inputs, such as email, date, number, range, color and new attributes such as placeholder, required, pattern, autofocus.
    • HTML5 adds client-side storage like localStorage, sessionStorage, IndexedDB, application cache / service workers while HTML had only cookies.
    • HTML5 supports multimedia without plugins like Flash.
    • HTML5 adds <canvas> for drawing graphics and animations and improves SVG support.

Vitual Environment

Python recommends using a virtual environment to build Python applications.

A virtual environment is an isolated environment having its copy of the interpreter and libraries so that there's no clash with the global installation of Python.

Python's virtual environment is set-up with the help of a built-in module named venv.

Django Project Structure

  • In Django,
    • a project represents the entire web application.
    • an app is a sub-module of a project.
  • An app is typically used to implement functionality for some specific purpose.
    • apps can be self-contained, meaning they do not rely on other apps to function.
    • apps can be used or reused in may different projects. This leads nicely to the DRY principle.
    • an app should be feature targeted, and it's best suited for one and only one thing.
  • In bref, a Django web application is a project that contains many apps.

DRY Principle (Don't Repeat Yourself)

The DRY principle stands for Don't Repeat Yourself. It's a fundamental guideline in software development that says:

Every piece of knowledge should have a single, unambiguous, authoritative representation within a system.

In simple terms, it prevents duplicating code, logic, or data.

  • The DRY principle leads to less maintenance, fewer bugs, and better readability.
  • The DRY principle applies to coding and refactoring, database schema design, API design, infrastructure/configuration, and documentation.

What is a project?

  • A Django project is a Python package containing the database configuration used by various sub-modules (apps) and other Django-specific settings.

  • The startproject command of django-admin is used to create a new Django project. It creates the folder of the given name (is called project directory), inside which there is another folder of the same name (is called project package) and the script manage.py.

    > django-admin startproject <project_name>
    • Project directory is created when we create a Django project. It contains manage.py and project package folder.
    • Project package contains a settings.py file and other files.
    • By convention, project names use lowercase with no spaces or hyphens. Underscores are allowed, but generally avoided. For example: the Little Lemon project should be named littlelemon.
  • The manage.py script has the same role as the django-admin utility. It can perform everything that the django-admin utility does. However, using manage.py is more straightforward, especially if we are required to work on a single project.

  • The startapp command is used to create a new app. An app is also represented by a folder of a specific file system.

    > python manage.py startapp <app_name>

    By convention, app names use snake_case and singular nouns. For example: book, demo_app.

  • Django manages the database operations with the ORM technique.

  • Migration refers to generating a database table whose structure matches the data model declared in the app.

    > python manage.py makemigration
  • The migrate command synchronizes the database state with the currently declared models and migrations.

    > python manage.py migrate
  • The runserver command starts Django's built-in development server on the local machine.

    > python manage.py runserver

Project package

When a project is created, the inner folder with (the same project name) is a Python package. The startproject template places 4 more files in the package folder.

For a folder to be recognized by Python as a package, it must have a file __init__.py.

  • settings.py contains configuration settings for the Django project, including the INSTALLED_APPS list where newly created apps must be added.
  • urls.py defines the URL patterns for both the project and the app, routing requests to the appropriate view functions. Every time the client browser requests a URL, the Django server looks to match its pattern and routes the application to the mapped view.
  • asgi.py is used by the application servers following the ASGI standard to serve asynchronous web applications.
  • wsgi.py is the entry point for such WSGI-compatible servers to serve classical web application.

Object-Relational Mapping (ORM)

ORM stands for Object-Relational Mapping. It's a programming techinque used to interact with a relational database (like PostgreSQL, MySQL, or SQLite) using objects in a programming language instead of writing raw SQL queries.

ORM automatically maps:

  • Database tables -> Classes.
  • Rows -> Objects.
  • Columns -> Object attributes.

The ORM internally generates and run the SQL.

Benefits of ORM:

  • Less SQL. We work mainly with our language's objects, not manual SQL strings.
  • Faster development. CRUD operations are simplified.
  • Database independence. Most ORMs work with many database engines.
  • Security. ORMs help prevent SQL injection by parameterizing queries.
  • Maintainable code. Models and relationships are clean and structured.

Donwsides of ORM:

  • Can be slower than optimized SQL queries.
  • May hide what queries are actually being executed.
  • Complex queries sometimes require raw SQL anyway.

Web Server Gateway Interface (WSGI)

WSGI stands for Web Server Gateway Interface. It's a Python web standard (specification) that defines how Python web applications communicate with web servers.

Before WSGI, every framework and server had its own protocol, nothing was compatible. WSGI unified everything. It allows any WSGI-compatible framework (Flask, Django <=2.1, Pyramid) to run on any WSGI-compatible server (Gunicorn, uWSGI, mod_wsgi).

WSGI is synchronous and designed for traditional HTTP request/response cycles: no async, no WebSockets, no long-lived connections.

A WSGI server is a program that implements the WSGI specification and runs a Python WSGI application. It handles:

  • receiving HTTP requests from clients.
  • passing them to the Python application via the WSGI interface.
  • returning the responses to the client.

Client-Application Request-Response Cycle

Asynchronous Server Gateway Interface (ASGI)

ASGI stands for Asynchronous Server Gateway Interface. It's also a Python web standard that defines how web servers comunicate with Python applications, similar to WSGI, but designed for async use cases.

Mordern apps need WebSockets, long-runing connections, non-blocking async I/O, concurrency without threads, so ASGI was created to support both synchronous and asynchronous Python code, including real-time features.

ASGI is a specification, not code. It defines a common interface between ASGI servers (e.g., Uvicorn) and ASGI applications (e.g., FastAPI, Django 3+).

An ASGI server is a program that implements the ASGI specification and runs ASGI-compatible Python app.

ASGI is the modern Python web standard for async apps.

  • It supports both synchronous and asynchronous code.
  • It enables WebSockets, streaming, and high concurrency.

Synchronous vs. Asynchronous Web Apps

  • A synchronous web app handles one request at a time per worker, following a simple request -> process -> response pattern. Its characteristics:
    • Blocking I/O: while a request is being processed, the worker can't handle another.
    • Thread/process based concurrency: to handle more users, it needs to add more worker processes or threads.
    • Straightforward code: no async/await.
    • Great for CPU-bound or simple I/O-bound actions.
  • An asynchronous web app handles requests using an event loop, allowing a single worker to server thousands of connections without blocking. Its characteristics:
    • Non-blocking I/O: tasks pause with await while waiting such as Database I/O, HTTP calls, File system I/O, WebSockets.
    • Concurrency through async/await, not threads.
    • Ideal for high-scale or real-time applications.
  • Async shines when we have lots of waiting, not lots of computing.

Concurrency vs. Parallelism

  • Simple definition:
    • Concurrency = doing many things seemingly at the same time.
      • Tasks overlap in time.
      • A single worker switches between tasks.
      • Like multitasking.
    • Parallelism = doing many things exactly at the same time.
      • Tasks run at the same instant.
      • Requires multiple CPU cores, multiple workers.
  • Technical definition:
    • Concurrency: multiple tasks make progress during overlapping time periods.
      • does not require multiple cores.
      • achieved through: async/await (event loop), coroutines, cooperative multitasking, context switching.
    • Parallelism: multiple tasks execute at exactly the same moment.
      • requires multiple CPU cores, multiple processes, CPU parallelism.
  • In Python:
    • Concurrency helps with I/O-bound tasks like web requests, database calls, file reads, sleep timers. Examples:
      • asyncio (single-thread event loop),
      • threading (even though GIL limits CPU parallelism),
      • non-blocking I/O.
    • Parallelism helps with CPU-bound tasks like heavy computations, machine learning workloads, image processing, compression/encryption. Examples:
      • multiprocessing,
      • C-extension parallel code,
      • NumPy operations (internally parallel).

django-admin vs. manage.py commands

  • Both can be used to perform the same tasks, but there are some subtle differences, and the choice of usage will depend on how we want to work on project.

  • django-admin is Django's command line utility for administrative tasks. This utility is present in the scripts folder of the Django environment directory. django-admin utility is executed from inside the terminal.

    It can also be launched via the call of module python -m django.

  • manage.py is a script that is the local version of django-admin and is located inside the project folder. It sets the Django settings module environment variable so that it points to our project settings.py file.

  • manage.py is a file that is automatically created each time we create a Django project, it is specific to the virtual environment of the project.

  • When working on a single Django project, developers tend to use manage.py.

  • However, if we need to switch between multiple Django settings files, use the django-admin command with Django settings module or the settings command line option.

manage.py is more convenient to use than django-admin. It runs inside the project folder. When using django-admin, you must set --settings variable to the required project's settings.py file.

App structure

  • An app is responsible for performing one single task out of the many involved in the complete web application, represented by the Django project.

  • The startapp command option of the manage.py script creates a default folder structure for the app of that name.

    > python manage.py startapp <app_name>
  • The folder structure looks like this

    demoproject/
    ├── db.sqlite3
    ├── manage.py
    ├── demo_app/
    │   ├── admin.py
    │   ├── apps.py
    │   ├── models.py
    │   ├── tests.py
    │   ├── views.py
    │   ├── __init__.py
    │   └── migrations/
    │       └── __init__.py
    └── demoproject/
        ├── asgi.py
        ├── settings.py
        ├── urls.py
        ├── wsgi.py
        └── __init__.py
    
  • views.py

    A view is a user-defined function that's called when Django's URL dispatcher identifies the client's request URL and matches it with a URL pattern defined in the urls.py file.

  • models.py. The data models required for processing in the app are created in this file.

    A data model is a Python class based on django.db.models class. All the models present here are migrated to the database tables.

Web Framework

  • Frameworks are designed to support the developer in building the web application.
  • The purpose of a web framework is to make application development easier and to provide the developer with a clean structure that keeps things in order and allows for changes and modifications.
  • Frameworks also allow for code reusability facilitated by existing code. They provide a solid foundation on which to build web application.
  • A web application is split into two parts:
    • Front-end is the part of the website that the user interacts with via web browser.
    • Back-end is the part that runs on a web server and usually contains a database.

Three-tier Architecture

  • Architecture refers to the fundamental structures of a software system.
  • Three-tier architecture is a modular based approach to client-server architecture that splits the application into three logical parts:
    • the presentation tier is the layer the users primarily interact with through user interfaces from their desktop, laptop, or mobile devices. It's commonly built with a UI framework or library such as React, and it communicates with other tiers by sending results through the application interface.
    • the data tier usually consists of database servers for storing and retrieving information.
    • the application tier is what ties together the other two tiers. It gets data from the presentation layer and persists it in the data tier.

MVC Architecture (Model-View-Controller)

  • Most of the web frameworks implement the MVC (Model-View-Controller) architecture.
  • The MVC design pattern separates the entire web application development process into three layers: Model, View, and Controller.
    • The Controller intercepts the user requests. It coordinates with the View and Model layers to send the appropriate response back to the client.
    • The Model is responsible for data definitions, processing logic and interaction with the backend database.
    • The View is the representation layer of the application. It takes care of the placement and formatting of the result and sends it to the Controller, which in turn, redirects it to the client as the application's response.

MVT Architecture (Model-View-Template)

  • The Django framework adapts a Model-View-Template (MVT) approach, a slight variation of the MVC approach.
  • A Django application consists of four following components:
    • URL Dispatcher is the entry point that decides which part of the application handles the request. The urls.py module acts as the dispatcher. It defines the URL patterns. Each URL pattern is mapped with a view function.

      When the server receives a request in the client URL, the dispatcher matches its pattern with the patterns available in the urls.py module.

      It then routes the flow of the application toward its associated view.

    • The View function reads the path, query, and body parameters included in the client's request. It uses the client's and the model's data and renders its response using a template.

      If required, it uses this data to interact with the models to perform CRUD operations.

      Django's View layer performs the role of Controller in MVC architecture.

    • A Model is a Python class. An app may have one or more model classes, conventionally put in the models.py file.

      Django migrates the attributes of the model class to construct a database table of a matching structure.

      Django's ORM (Object Relational Mapper) helps perform CRUD operations in an object-oriented way instead of invoking SQL queries.

    • A Template is a web page containing a mix of static HTML and Django Template Language code blocks. It is equivalent to the View in the MVC architecture.

      Django's template processor uses any context data from the view inserted in these blocks to formulate a dynamic response.

Views

  • The primary role of a view function is to fetch the data from the client's request, apply the necessary processing logic, and return an appropriate response to the client.

    A view function is a Python function that handles a web request and returns a web response.

  • It receives the request as an HttpRequest object, and returns an HttpResponse object containing the response body, status code, and any relevant headers.

  • View functions often:

    • handle GET/POST requests,
    • validate forms,
    • query models,
    • redirect users,
    • return JSON (for APIs).
  • Best practice: view functions are placed in the application's views.py module.

    For example:

    # views.py
    from django.shortcuts import render
    from django.http import HttpResponse, HttpRequest
    
    
    def home(request: HttpRequest) -> HttpResponse:
      return HttpResponse("Hello World!")
    
    
    def welcome(request: HttpRequest) -> HttpResponse:
      context = {"name": "Alice"}
      return render(request, "welcome.html", context)
  • View functions need to be mapped to specific URLs, ensuring that Django calls the appropriate view when a request targets that URL.

Class-Based Views

  • Class-based views are views written as classes, instead of functions (function-based views).

  • Class-based views repond to HTTP requests using class instance methods: get, post, put, delete, patch.

    For example:

    # views.py
    from django.views import View
    from django.http import HttpRequest, HttpResponse
    
    
    class HomeView(View):
      def get(self, request: HttpRequest) -> HttpResponse:
        return HttpResponse("Response to GET request")
    
      def post(self, request: HttpRequest) -> HttpResponse:
        return HttpResponse("Response to POST request")
  • They allow to structure view logic in an object-oriented way, making code more reusable, organized, and extensible.

    • code reusability: we can create base classes and let other views inherit behavior.
    • cleaner and orgnized code: logic is grouped into class methods instead of long function-based views.
    • extensiblity: we can override just the parts we need.
  • Django provides many built-in generic views in the django.views.generic module. These class-based views simplify the process of declaring view patterns and reduce the amount of boilerplate code we need to write.

    • ListView: displays a list of objects. For example:

      # views.py
      from django.views.generic import ListView
      from .models import Employee
      
      
      class EmployeeListView(ListView):
        model = Employee
        template_name = "employees/employee_list.html"
        context_object_name = "employees"
      
      
      # urls.py
      from django.urls import path
      from .views import EmployeeListView
      
      
      urlpatterns = [
        path("employees/", EmployeeListView.as_view(), name="employee_list"),
      ]
      <!-- employees/employee_list.html -->
      <ul>
        {% for employee in employees %}
        <li>Name: {{ employee.name }}</li>
        <li>Email: {{ employee.email }}</li>
        <li>contact: {{ employee.contact }}</li>
        <br/>
        {% endfor %}
      </ul>
    • DetailView: displays details of a single object. For example:

      # views.py
      from django.views.generic import DetailView
      from .models import Employee
      
      
      class EmployeeDetailView(DetailView):
        model = Employee
        template_name = "employees/employee_detail.html"
        context_object_name = 'employee'
      
      
      # urls.py
      from django.urls import path
      from .views import EmployeeDetailView
      
      
      urlpatterns = [
        path("employees/<int:pk>/", EmployeeDetailView.as_view(), name="employee_detail"),
      ]
      <!-- employees/employee_detail.html -->
      <h1>Name : {{employee.name}}</h1>
      <p>Email : {{ employee.email }}</p>
      <p>Contact : {{ employee.contact }}</p>
    • CreateView: creates a new object using a form. For example:

      # views.py
      from django.views.generic import CreateView
      from .models import Employee
      
      
      class EmployeeCreateView(CreateView):
        model = Employee
        fields = "__all__"
        template_name = "employees/employee_form.html"
        success_url = "/employees/"
      
      
      # urls.py
      from django.urls import path
      from .views import EmployeeCreateView
      
      
      urlpatterns = [
        path("employees/create/", EmployeeCreateView.as_view(), name="employee_create"),
      ]
      <!-- employees/employee_form.html -->
      <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Save">
      </form>
    • UpdateView: updates an existing object. For example:

      # views.py
      from django.views.generic import UpdateView
      from .models import Employee
      
      
      class EmployeeUpdateView(UpdateView):
        model = Employee
        fields = "__all__"
        template_name = "employees/employee_form.html"
      
      
      # urls.py
      from django.urls import path
      from .views import EmployeeUpdateView
      
      
      urlpatterns = [
        path("employees/<int:pk>/update/", EmployeeUpdateView.as_view(), name="employee_update"),
      ]
    • DeleteView: deletes an existing object with confirmation. For example:

      # views.py
      from django.views.generic import DeleteView
      from django.urls import reverse_lazy
      
      from .models import Employee
      
      
      class EmployeeDeleteView(DeleteView):
        model = Employee
        template_name = "employees/employee_confirm_detele.html"
        context_object_name = 'employee'
        success_url = reverse_lazy("employee_list")
      
      
      # urls.py
      from django.urls import path
      from .views import EmployeeDeleteView
      
      
      urlpatterns = [
        path("employees/<int:pk>/delete/", EmployeeDeleteView.as_view(), name="employee_delete"),
      ]
      <!-- employees/employee_confirm_detele.html -->
      <form method="post">
        {% csrf_token %}
        <p>Are you sure you want to delete "{{ employee.name }}"?</p>
        <input type="submit" value="Confirm">
      </form>
    • TemplateView: renders a static template (no database). For example:

      # views.py
      from django.views.generic import TemplateView
      
      
      class AboutView(TemplateView):
        template_name = "about.html"
      
      
      # urls.py
      from django.urls import path
      from .views import AboutView
      
      
      urlpatterns = [
        path("about/", AboutView.as_view(), name="about"),
      ]
      <!-- about.html -->
      <h1>About Us</h1>
      <p>Welcome to our company.</p>
  • Class-based views allow inheritance and mixins.

    • A mixin is a class designed to be inherited alongside another class to add extra features, but not mean to stand alone.

    • Mixins are reusable, contain small, focused logic, allow to combine behaviors cleanly.

    • When using mixins, always place them before the view class so that Python's MRO (Method Resolution Order) to find the mixin methods first. For example:

      # views.py
      from django.views.generic import TemplateView
      
      
      class TitleMixin:
        title = ""
      
        def get_context_data(self, **kwargs):
          context = super().get_context_data(**kwargs)
          context["title"] = self.title
          return context
      
      
      class HomeView(TitleMixin, TemplateView):
        template_view = "home.html"
        title = "Home Page"

Function-Based Views vs. Class-Based Views

  • The choice of function-based views and class-based view depends on complexity, reusability, and clarity.
    • if the view is simple -> use function-based views.
    • if the view is complex or reusable -> use class-based views.
  • Use function-based views when:
    • simple view: returns a template, handles one request method, has straightforward behavior.
    • no need for inheritance, need maximum transparency and control.
    • the view is small and not reused.
    • prefer direct control.
  • Use class-based views when:
    • need to handle multiple HTTP methods cleanly.
    • want to reuse or extend behavior.
    • using generic views such as ListView, DetailView, CreateView, UpdateView, DeleteView, FormView.
    • need cleaner, structured code. Class-based views break behavior into clear override-able methods.
    • need mixins: authentication, permissions, etc.

HyperText Transfer Protocol (HTTP)

  • HTTP stands for HyperText Transfer Protocol.
  • HTTP is a core operational protocol of the world wide web. It enables a web browser to comunicate with a web server.
  • HTTP is a request-response based protocol. It works with a client -> request -> server -> response cycle.
    • A client (web browser) sends the HTTP request to a server.
    • The web server sends the HTTP response back to the browser.
  • HTTP is used for almost all communication on the web, including: loading web pages, APIs and webservices, file transfers, form submissions, and so on.
  • It's a stateless protocol.
    • Each HTTP request is independent.
    • Servers do not remember past requests unless cookies or sessions are used.

HTTP Request

An example of a HTTP request:

GET / HTTP/1.1
Host: developer.mozilla.org
Accept-Language: en

A HTTP request consists of:

  • a method, e.g., GET

  • a path (resource location), e.g., /

  • a version, e.g., HTTP/1.1

  • headers contain additional information about the request and the client that is making the request. Headers can contain information such as the server name, the server port, the request method type, and the content type. The content of the header can depend on the specific client and server. For example:

    Host: developer.mozilla.org
    Accept-Language: en
    
  • and optional body of content that the client is sending (for certain request methods like POST, PUT).

HTTP Response

  • HTTP responses follow a format similar to the request format.

    HTTP/1.1 200 OK
    Date: Sat, 09 Oct 2010 14:28:02 GMT
    Server: Apache
    Last-Modified: Tue, 01 Dec 2009 20:18:22 GMT
    ETag: "51142bc1-7449-479b075b289I1b"
    Accept-Ranges: bytes
    Content-Length: 29769
    Content-Type: text/html
    
  • Following the header, the response will optionally contain a message body consisting of the response content, such as the HTML document, the image file, and so forth.

    <html>
      <body>
        <p>Hello world!</p>
      </body>
    </html>
  • HTTP status code, e.g., 200, contained within the header indicate if the HTTP request successfully completed. The code values are in the range of 100-599 and are grouped by purpose.

  • The status message, e.g., OK, is a text representation of the status code.

HTTP Methods

  • HTTP mehod describes the type of action that client wants to perform and comunicates it to the server.

  • The primary or the most commonly used HTTP methods are: GET, POST, PUT, PATCH, and DELETE.

  • GET method:

    • is used to retrieve information from the given server.
    • is safe. It does not change server data.
    • is idempotent. The same request yields the same result.
    • data is sent in the URL (query string).
    • should not be used for sensitive data.

    For example: the following request retrieves user with id 5.

    GET /users/5
    
  • POST method:

    • is used to create new data on the server.
    • is NOT idempotent. Sending a same request twice may create duplicates.
    • data is sent in the body.
    • is used for new submissions, uploads, form data.

    For example: the following request creates a new user.

    POST /users
    {
      "name": "John"
    }
    
  • PUT method:

    • is used to fully update an existing resource.
    • is idempotent. The same request yields the same result.
    • replaces the entire resource unless implemented otherwise.
    • must include the full updated data. If any field is missing, it may be overwritten or remove.

    For example: the following request replaces user with id 5 with the provided data.

    PUT /users/5
    {
      "name": "John",
      "age": 25
    }
    
  • PATCH method:

    • is used to partially update the resource. It tells the server to update only the provided fields.
    • is idempotent. The same request yields the same result.

    For example: the following request updates only the age of the user with id 5.

    PATCH /users/5
    {
      "age": 25
    }
    
  • DELETE method:

    • is used to remove a resource.
    • is idempotent. Deleting the same item repeatedly gives the same result.
    • removes data from the server.

    For example: the following request deletes the user with id 5.

      DELTE /users/5
    

Note that HTTP methods are only conventions, not enforcement. The developer's code determines whether the operation is actually idempotent.

HTTP Request-Response Cycle

HTTP Request-Response Cycle

  • Browser (Client) / App Layer: Discovers resources, sends requests, parses HTML, and renders the page.
  • HTTP Layer: Manages request/response. In HTTP/2, HTTP/3 it also handles streams, frames, and multiplexing.
  • Transport Layer: Responsible for data transport: splits the byte stream into packets and ensures ordered, reliable delivery (no data loss).
  • Network Layer: Transmits packets over the Internet (routers, switches, etc.).

HTTP Versions

The three most commonly used HTTP versions are HTTP/1.1, HTTP/2, and HTTP/3.

  • HTTP/1.1
    • Text-based protocol. It means messages are written in human-readable plain text.
    • One request per TCP connection (unless using keep-alive).
    • If one request is delayed, others are blocked due to HOL (Head-of-Line) blocking.
    • Browser decides whether to open one or multiple connections based on its connection pool (typically up to ~6 connections per domain) to send requests in parallel.
    • Pros:
      • Simple, widely supported.
      • Works everywhere, even on very old systems.
    • Cons:
      • Significant latency with many small resources (e.g., 100+ assets per page).
      • Inefficient for modern web workloads.
  • HTTP/2
    • Binary framing layer. It means the protocol uses structured binary data frames (machine-readable packages) instead of text. It's more compact than text and is faster to parse.
    • Multiplexing: multiple simultaneous streams over a single TCP connection.
    • Header compression (HPACK): smaller request -> faster transfers.
    • Stream prioritization.
    • Faster than HTTP/1.1 when network quality is good.
    • Still suffers from TCP-level HOL blocking:
      • If packets are lost, the entire connection stalls.
      • Multiplexing doesn't help because they share one TCP connection.
    • Pros:
      • Low latency.
      • More efficient for complex sites.
      • Widespread support.
    • Cons:
      • Performance drops significantly on mobile or unstable networks.
  • HTTP/3
    • Binary framing layer.
    • Runs over QUIC, which is built on UDP instead of TCP.
    • QUIC includes:
      • Built-in TLS 1.3 encryption.
      • Stream-level flow control.
      • Connection migration. Keep connection alive when IP changes, helpful for mobile.
    • No TCP HOL blocking -> stream are independent.
    • Faster connection setup:
      • No separate TCP + TLS handshake.
      • Often 0-RTT (zero round-trip time) startup.
    • Handles packet loss gracefully.
    • Pros:
      • Best for modern mobile networks.
      • Extremely fast in high-latency environments.
      • Robust when switching networks, e.g., Wi-Fi -> mobile data.
    • Cons:
      • Still rolling out globally.
      • Firewalls and enterprise networks sometimes block UDP.

HOL Blocking

  • HOL blocking stands for Head-Of-Line Blocking.
  • It is a performance problem that occurs in network protocols when one slow or lost packet blocks all the packets behind it, even if those later packets could otherwise have been processed.
  • In HTTP/1.1
    • Each TCP connection handles one request at a time.
    • If one request is slow, every request behind it in that connection waits.
    • Browsers open many parallel connections to reduce this problem.
  • In HTTP/2
    • Supports multiplexing, multiple streams on one connection.
    • Still uses TCP, which has packet-level HOL blocking:
      • If one TCP packet is lost, TCP must wait and retransmit it.
      • All HTTP/2 streams on that connection pause until the packet is recovered.
  • In HTTP/3
    • No TCP HOL blocking.
    • Uses QUIC, built on UDP, handles streams independently.
    • If one packet is lost:
      • Only the affected streams waits.
      • All other streams continue normally.
  • HOL blocking makes website slower because:
    • A single lost packet affects all streams, requests behind it.
    • high-latency or mobile networks suffer more.
    • performance degrades for real-time or resource-heavy website.
  • TCP has HOL blocking because:
    • TCP enforces strict, in-order delivery.
    • Treats the connection as one continuous byte stream.
    • If one packet is lost -> whole connection halts.
  • QUIC solves TCP's connection-wide HOL blocking problem:
    • Built-on UDP -> QUIC controls ordering + reliability itself.
    • Multiple independent streams inside one connection.
    • Packet loss affects only the stream involved.

HTTP Status Codes

  • HTTP status codes are three-digit numbers that a web server sends back in an HTTP response to tell the client (browser, app, API, etc.) what happened to its request.
  • They are grouped into five categories, each representing a different class of response.
  • 1xx - Informational indicates that the request was received and is still being process. The most common informational responses are:
    • 100 Continue: server acknowledges request headers, client can send body.
    • 101 Switching Protocols: server is switching protocols, e.g., to WebSocket.
    • 102 Processing: server is working but not finished. It's not a final response, it's sent before the final status code to prevent the client from timing out while the server is doing something that takes a long time, e.g., large file operations, deep searches, etc.
  • 2xx - Success indicates that the request was successfully processed by the server. The most common success responses are:
    • 200 OK: standard success response.
    • 201 Created: a new resource was created, e.g., after POST.
    • 202 Accepted: the server acccepted the request, but has not processed it yet and may process it later. It's often used in APIs, for asynchronous processing (background jobs).
    • 204 No Content: success, but no response body, common for DELETE.
  • 3xx - Redirection indicates to the client that the requested resource has been moved to a different path. Browsers, and most HTTP clients automatically follow new URL unless users explicitly disable that behavior. The most common redirection responses are:
    • 301 Moved Permanently: resource moved to a new permanent URL, method might change.
    • 302 Found: temporary redirect, method might change.
    • 304 Not Modified: client can use a cached version.
    • 307 Temporary Redirect: like 302, but method must not change.
    • 308 Permanent Redirect: like 301 but method must not change.
  • 4xx - Client Errors indicates that the client made a bad request. The most common client errors responses are:
    • 400 Bad Request: the request was malformed.
    • 401 Unauthorized: authentification is required.
    • 403 Forbidden: authentification OK, but access denied.
    • 404 Not Found: resource not found.
    • 405 Method Not Allowed: request method not allowed. It means the server understands the method but that method is not allowed for this specific resource.
    • 409 Conflict: resource conflict, e.g., duplicate data.
    • 429 Too Many Requests: rate limiting.
  • 5xx - Server Errors indicates that the server failed to process a valid request. The most common server errors responses are:
    • 500 Internal Server Error: generic server failure. It means the server encountered an unexpected condition and could not fulfill the request.
    • 501 Not Implemented: server doesn't support the requested method. It means the server does NOT recognize the method.
    • 502 Bad Gateway: indicates a problem between servers. A server acting as a gateway or proxy received an invalid response from an upstream server.
    • 503 Service Unavailable: server overloaded or down for maintenance.
    • 504 Gateway Timeout: upstream server didn't respond in time.

HTTP Secure (HTTPS)

  • HTTPS stands for HTTP Secure.
  • Uses SSL/TLS encryption.
    • SSL (Secure Sockets Layer) is deprecated and insecure now. SSL is completely disabled in modern browsers, severs.
    • TLS (Transport Layer Security) is the newer and current security protocol. Only TLS 1.2 and TLS 1.3 are recommended today.
  • Benefits:
    • Data is encrypted, so attacker can't read or tamper with it.
    • Requires an SSL/TLS certificate issued by a trusted Certificate Authority (CA).
    • Ensures data integrity. Information arrives unchanged.
    • Protects user privacy by encrypting all transmitted data.
  • How HTTPS works:
    1. client -> server: "Hello!". When we visit a HTTPS site, the browser sends the server a hello message which contains:
      • supported encryption methods,
      • supported TLS versions,
      • a random number, used to generate keys later.
    2. server -> client: "Here's my certificate". The server replies with a message which contains:
      • its SSL/TLS certificate,
      • its public keys,
      • a random number of its own.
    3. the browser checks if the certificate is valid and trusted, then creates a session key:
      • generates a secret symmetric key.
      • encrypts this key with the server's public key.
      • sends it back to the server. Only the server can decrypt this because it has the private key.
    4. secure encrypted tunnel is establish.
      • Now, both browser and server share the same secrete session key.
      • They use symmetric encryption to exchange data securely.
    5. encrypted data transfer begins. Every request/response is encrypted: URLs (except domain), cookies, form data, API calls, headers (partially).
    6. for every record, the client includes a cryptographic checksum, generated using the session key, associated with that data. When the serve receives the record, it recomputes the checksum and compares it to the transmitted value. If they don't match, the message is rejected. This ensures data integrity.

Request and Response Objects

  • Django handles the request and response with the help of HttpRequest and HttpResponse classes in the django.http module.
  • Django obtains the HttpRequest object from the context provided by the server.
  • As a client's request received, Django's URL dispatcher mechanism invokes a view that matches the URL pattern and passes this HttpRequest object as the first argument so that all the request metadata is available to the view for processing.

HttpRequest Object

The HttpRequest object contains metadata about the client's request, including method, GET and POST parameters, cookie, and user information. Some of the main attributes and methods of an HttpRequest object (e.g., request) are:

  • request.method returns the HTTP method that the client used to send request to the server.
  • request.GET and request.POST return a dictionary-like object containing GET and POST parameters, respectively.
  • request.COOKIES returns a dictionary of string keys and values.
  • request.FILES: when user uploads one or more files with a multipart form, they're present in this attribute in the form of UploadedFile objects.
  • request.user contains information about the current user. It's an object of django.contrib.auth.models.User class. If the user is unauthenticated, it returns AnonymousUser.
  • request.has_key() helps check whether the GET or POST parameter dictionary has a value for the given key.

HttpResponse Object

The HttpResponse object is used to construct the response sent back to the client, including status codes, content, and headers. Some of the main attributes and methods of the HttpResponse object are:

  • status_code returns the HTTP status code corresponding to the response.
  • content returns the byte string of the response.
  • write() creates a file-like object.

Understanding URLs

URL stands for Uniform Resource Locator. It's simply an address where the files are stored. For example:

  • https://www.littlelemon.com/customers/5.
  • https://www.littlelemon.com/menu/?year=2022.

A URL is made up of multiple parts put together:

  • scheme or referred as the protocol is located at the beginning of any url address and can be identified as http or https. The protocol determines the set of rules around the transmission and exchange data.
  • subdomain is located before the domain and usually contains the home page and other important pages. The most common subdomain is World Wide Web represented by www.
  • domain, e.g., littlelemon.com, consists of two parts:
    • second level domain refers to an organization or the name of a company. e.g., littlelemon.
    • top level domain is used to reference a country or category of the organization. e.g., .com address can indicate a comercial entity.
  • path also known as the page path directs the user to the location of a resource. e.g., /customers/5, /menu.
  • query string begins with a question mark symbol ? and is placed after the URL path. It contains parameters represented as key value pairs. e.g., ?year=2022.

Parameters

The view function in Django receives its mandatory argument as the request object from the server context. The client may pass additional arguments via different methods.

Path Parameter

  • A path parameter is a variable part of the URL that is used to identify a specific resource, such as /customers/5, where 5 is an argument of the path parameter.
  • There may be multiple path parameters in the URL, separated by the path separtor, the slash symbol /.
  • How it works:
    • The URL dispatcher maps the pattern to the view function and identifies 5 as the customer id pk parameter.

      path("custmers/<int:pk>/", views.customer_detail, name="customer_detail"),
    • The parameter is parsed as pk parameter and picked by the views.customer_detail() function.

    • The view customer_detail function needs an additional parameter pk, as shown in the following example, because an argument was passed inside the urls.py file.

      def customer_detail(request: HttpRequest, pk: int):
        pass
    • The parameter names added inside the path function in the urls.py file must match the ones added inside the customer_detail() view function associated with it in the views.py file.

    • Best pratice: avoid overly verbose parameter names such as customer_id, prefer the conventional pk.

Path Converters

  • The URL pattern treats the identifiers in angular brackets <> as the path parameters.
  • By default, it parses the received value to the string type.
  • Path parameters avaiblable are:
    • str: matches any non-empty string and excludes the path separator /. This is the default if a converter isn't included in the expression.
    • path: matches any non-empty string and includes the path separator /.
    • int: matches zero or any positive integer and returns an int.
    • uuid: matches a formatted UUID and returns a UUID instance.
    • slug: matches any slug string consisting of ASCII letters or numbers, including the hyphen and underscore characters.

Query Parameter

  • A query string is a sequence of one or more key-value pairs concatenated by the ampersand symbol &. They're added to the URL after a question mark symbol ?.

    For example: https://www.littlelemon.com/customers/?name=John&age=35

  • The URL dispatcher doesn't parse these parameters. They are fetched by the view function from the request object it receives.

  • The key-value pairs in the query string are added to the request.GET property. The request object's GET property is a dictionary-like object. Hence, values can be get as shown in the following example.

    def customers(request: HttpRequest):
      name = request.GET.get("name")
      age = request.GET.get("age")

Body Parameter

  • Body parameters are data sent in the body of a POST request, typically from an HTML form, which is not visible in the URL.

  • Values can be get via request object's POST progerty, as demonstrated below.

    def customers(request: HttpRequest):
      name = request.POST.get("name")
      age = request.POST.get("age")

URL Dispatcher

  • URL dispatcher is Django's mechanism that uses patterns that are defined by URL mapping in urls.py to route request to the correct view.
  • How it works:
    1. a request comes in, e.g., /customers/5/.
    2. Django removes the domain name and leading slashes.
    3. URL dispatcher looks at the urlpatterns list in urls.py file(s).
    4. It checks each pattern, from top to bottom.
    5. The first matching pattern triggers the corresponding view.
    6. Django calls that view and returns the response.

URL Mapping

  • URL mapping is a set of URL patterns that are defined in urls.py file(s). It's a list of instruction or a table of routes.

  • Components of URL mapping:

    1. URL patterns: written in urls.py using path() or re_path() function.
    2. Views: functions or classes that handle the request.
    3. Arguments / Parameters: dynamic segments like <int> or <slug>.
    4. Names: each URL can be given a name for reverse URL lookup.

    For example:

    path("home/", views.HomeView.as_view(), name="home")  # class-based view
    path("articles/<int:year>/<slug:title>/", views.articles, name="articles")  # function-based view

Regular Expressions in URLs

  • Regular expressions are used to define, extract, and validate dynamic URL paths before they are sent to the associated view function.

  • To use regular expressions in URLs, it needs to import and use the re_path() function from the django.urls module.

    For example:

    from django.urls import path, re_path
    
    from . import views
    
    
    urlpatterns = [
      path("about/", views.about, name="about"),
      path("menu-items/<int:pk>/", views.menu_item_list, name="menu_item_list"),
      re_path(r"^products/([0-9]{2})/$", views.product_list, name="product_list"),
    ]

URL Pattern Convention

Django follows a convention similar to directory in Unix:

  • ending pattern with a trailing slash: to look like a "container" endpoints. For example, "menu-items/10/".

    Django by default redirects URLs like example.com/menu-items/10 to example.com/menu-items/10/.

    Hence, the pattern menu-items/10/ works with both example.com/menu-items/10 and example.com/menu-items/10/, but menu-items/10 doesn't work with example.com/menu-items/10/.

  • NOT include a leading slash.

    Django does not expect leading slash, so /menu-item/10/ won't match example.com/menu-item/10/.

  • use kebab-case for URL paths. For example: menu-item.

Rule of thumb: never use leading slash, use trailing slash to keep consistency, use kebab-case for naming URL paths.

URL Namespacing

  • The application namespace is created by defining the app_name variable in the applications's urls.py module and assigning it the name of the app.

    # demo_app/urls.py
    app_name = "demo_app"
  • Django differentiates between same-name URLs in multiple apps with application namespace.

  • The app_name defines the application namespace so that the views in this app are identified by it.

    >>> reverse("demo_app:index")
    "/demo/"
  • We can also define the instance namespace in the include function while adding an app's urlpatterns. This namespace is called the instance namespace.

    urlpatterns = [
      path("demo/", include("demo_app.urls", namespace="demo_app"))
    ]
  • By convention, use snake_case for application namespace. For example: demo_app.

reverse Function

  • reverse() function does the opposite of URL matching. It takes a URL name (and optionally parameters) and returns the actual URL path as a string.

  • It's useful to:

    • avoid hard-coding URLs as strings.
    • keep URLs consistent even if our URL patterns change.
    • help when generating links inside views, models, forms, etc.

    For example:

    • URL name is defined in the urls.py module.

      path("menu-items/<int:pk>/<str:dish>/", views.menu_item_detail, name="menu_item_detail")
    • Using reverse() function in the views.py module to get the actual URL path.

      from django.urls import reverse
      
      url = reverse("menu_items", kwargs={"dish": "pasta", "pk": 10})
      print(url)  # /menu-items/10/pasta/
  • The reverse() function is commonly used:

    • in views to redirect.

      from django.shortcuts import redirect
      from django.urls import redirect
      
      return redirect(reverse("homepage"))
    • in templates, indirectly via {% url %}.

      <a href="{% url 'homepage' %}">Home</a>

      Note: {% url %} is a built-in Django template tag. It works out of the box in Django templates - no import and no {% load %} statement required.

    • in Django REST framework when building hyperlinks.

Error Handling

Django has a built-in error handling system that helps us manage exceptions, return proper error pages, and debug applications.

  • Django's built-in error views: Django automatically provides default pages for common HTTP errors such as 400, 403, 404, 500.

    • When DEBUG = True (development mode), Django shows a detailed debug page with traceback, request info, environment variables, template context.
    • When DEBUG = False (production mode), Django shows simple public-facing error pages (400.html, 403.html, etc.)
  • Custom error pages: we can override Django's default error pages by creating templates in the project /templates folder:

    templates/400.html
    templates/403.html
    templates/404.html
    templates/500.html
    
  • Custom error handlers: We may define customer view functions to handle errors in the urls.py module at the project level.

    # project/urls.py
    
    handler400 = "my_app.views.custom_400"
    handler403 = "my_app.views.custom_403"
    handler404 = "my_app.views.custom_404"
    handler500 = "my_app.views.custom_500"
    # my_app/views.py
    
    def custom_400(request, exception):
      return render(request, "400.html", status=400)
  • Inside views: we can return a HttpResponse or raise an exception. For example:

    • returns a HttpResponseNotFound, which is a subclass of HttpResponse that specifically indicates a 404 error. It internally sends an error code 404. Other predefined subclasses include HttpResponseBadRequest and HttpResponseForbidden.
    • raises a Http404 exception, which is a class defined in the django.core.exceptions module. Some important exception types are: ObjectDoesNotExist, EmptyResultSet, and FieldDoesNotExist.

Method Resolution Order (MRO)

  • Method Resolution Order (MRO) is the rule that Python uses to decide which class's method/attribute gets called first when multiple classes are involved, especially in multiple inheritance.

  • MRO becomes important when:

    • a class inherits from multiple parent classes.
    • two parents contains a method with the same name.
    • mixins are used.
    • want to know which super method is called next.
  • MRO decides the search path Python will follow.

    For example:

    class A:
      def hello(self):
        print("A")
    
    
    class B:
      def hello(self):
        print("B")
    
    
    class C(A, B):
      pass
    
    
    instance = C()
    instance.hello()  # A

    Even though the C class inherits from both A and B classes. Both of them have the hello method, Python chooses the one of A, not B. That decision is based on the MRO.

  • The built-in mro method is used to see the MRO. For example, C.mro() returns the following list, that is the exact search order Python uses.

    [
      <class '__main__.C'>,
      <class '__main__.A'>,
      <class '__main__.B'>,
      <class 'object'>
    ]
  • Python uses an algorithm called C3 Linearization to determine MRO:

    • Preserve the order of inheritance.
    • Respect MRO of parent classes.
    • Avoid inconsistency and conflicts.
    • Guarantee a single, predictable path (linear).

Naming Convention

  • Naming a view:

    • use snake_case for funtion-based views and PascalCase for class-based views.
    • use verbs or verb-noun phrases.
    • function-based view names should follow the pattern [resource]_[action].
    • class-based view names should end with View, following the pattern [Resource][Action]View.

    For example:

    # views.py
    def order_create(request: HttpRequest) -> HttpResponse:
      pass
    
    
    def customer_list(request: HttpRequest) -> HttpResponse:
      pass
    
    
    class OrderCreateView(View):
      pass
    
    
    class CustomerListView(View):
      pass
  • Naming URL patterns:

    • use kebab-case (hyphen-separated) for URL paths, snake_case for URL names (the name= argument).
    • use nouns, not verbs.
    • use plural nouns for list endpoints.
    • should describe the resource/action.

    For example:

    # urls.py
    urlpatterns = [
      path("orders/", views.order_list, name="order_list"),
      path("customers/", views.customer_list, name="customer_list")
    ]
  • Naming URL paths for CRUD operations: use the pattern [resources]/[instance]/[action] to keep routes predictable and consistent.

    For example:

    • employees/: list view
    • employees/<int:pk>/: detail view
    • employees/create/: create view
    • employees/<int:pk>/update/: update view
    • employees/<int:pk>/delete/: delete view
    # urls.py
    urlpatterns = [
      path("employees/", views.employee_list, name="employee_list"),
      path("employees/<int:pk>/", views.employee_detail, name="employee_detail"),
      path("employees/create/", views.employee_create, name="employee_create"),
      path("employees/<int:pk>/update/", views.employee_update, name="employee_update"),
      path("employees/<int:pk>/delete/", views.employee_delete, name="employee_delete"),
    ]
  • Naming a namespace (app_name):

    • use snake_case.
    • usually the app name.
    • lowercase.

    For example:

    # urls.py
    app_name = "demo_app"

In bref, use snake_case for URL names, function-based views and namespaces, kebab-case for URL paths, and PascalCase for class-based views.

Models

  • A model is the single definitive source of information about the data. It contains the essential fields and behaviors of the data.

    A model is a blueprint for a database table, written in Python.

  • Each model is a Python class that subclasses django.db.models.Model. A typical definition of a model class is done inside the app's models.py file. For example:

    from django.db import models
    
    
    class User(models.Model):
      first_name = models.CharField(max_length=30)
      last_name = models.CharField(max_length=30)
  • pk stands for primary key. It is a symbolic pointer to whatever field serves as the model's primary key. It is not a real model field, just a built-in alias. We can use pk in any ORM operations. For example:

    User.objects.get(pk=2)
    User.objects.filter(pk__in=[1, 2, 3])
  • id field: when declaring a model,

    • if no field is explicitly defined as the primary key, Django automatically creates an auto‑incrementing id field to serve as the primary key.
    • if a specific field is defined as the primary key, Django does not add the default id field.
  • A model:

    • represents a single database table.

    • each attribute of the model represents a database field.

    • each instance of the model is a row.

    • provides model methods to perform CRUD (Create, Read, Update, and Delete) operations using Django's ORM (Object-Relational Mapper).

      For example:

      • Create

        new_user = User(id=1, "John", "Jones")
        new_user.save()
      • Read

        user = User.objects.get(id=1)
      • Update

        user = User.objects.get(id=1)
        user.last_name = "Smith"
        user.save()
      • Delete

        User.objects.filter(id=1).delete()

Field Types

The django.models module has many field types to choose from.

  • CharField: is the most used field type. It can hold string data of length specified by max_length parameter.
  • TextField: is similar to CharField, but for a longer string.
  • IntegerField: stores an integer between $-2^{31}$ to $2^{31}-1$ (2_147_483_647). This limit comes from Django mapping IntegerField to the database INTEGER / INT data type, which is limited to 32 bits. Similar fields to store integers of varying lengths:
    • SmallIntegerField: stores an integer between $-2^{15}$ to $2^{15}-1$ (32_767).
    • BigIntegerField: stores an integer between $-2^{63}$ to $2^{63}-1$ (9.22e18).
    • PositiveIntegerField: stores an integer between $0$ to $2^{31}-1$. In fact, it store non-negative values.
    • AutoField: only used for primary key and auto-increment, stores an integer between $1$ to $2^{31}-1$.
  • FloatField: stores a floating-point number.
  • DecimalField: stores a number with fixed digits in the fractional part.
  • DateTimeField: stores the date and time as an object of Python's datetime.datetime class.
  • DateField: stores datetime.date value.
  • EmailField: is a CharField with an in-built EmailValidator.
  • URLField: is a CharField having in-built validation for URL.
  • FileField: used to save the file uploaded by the user to a designated path specified by the upload_to parameter.

Model Relationships

  • Primary Key is a unique identifier for each record in a database table, ensuring that no two rows have the same value.

  • Foreign Key is a field in one table that uniquely identifies a row of another table, establishing a relationship between two table.

  • The idea behind designing related tables is to avoid data redundancy, unnecessary repetition of the same data in many rows and ensure data integrity.

  • Relational databases have a mechanism to prevent the deletion of the primary key if it is being used in the related table so that the data integrity is intact.

  • There are three types of relationships that exists:

    • One-to-One relationship: a record in one model is associated with exactly one record in another model.

      For example, a college can have only one principal.

      from django.db import models
      
      
      class College(models.Model):
        name = models.CharField(max_length=50)
        strength = models.IntegerField()
        website = models.URLField()
      
      
      class Principal(models.Model):
        college_id = models.OneToOneField(College, on_delete=models.CASCADE)
        qualification = models.CharField(max_length=50)
        email = models.EmailField(max_length=50)

      Note: There are several reasons to use a One‑to‑One relationship instead of a single large table:

      • Separation of concerns: different data has different responsibilities. That keeps models smaller, responsibilities clearer, and code easier to reason about.
      • Optional or rare data: some data applies only to some users and are rarely accessed. Splitting avoids lots of NULL columns and keeps hot tables small and fast.
      • Different lifecycles: sometimes data is created later or can be deleted independently.
      • Permissions and ownership: some data should be accessible to different services and have different permissions. For instance, User table is accessible via auth service while UserProfile is accessible via profile service.
      • Database and performance reasons: some data like User is queried constantly while some other like UserProfile is queried occasionally. Splitting avoids wide rows, cache misses, and unnecessary I/O.
      • Domain modeling (real-world meaning): One-to-One represents a conceptual extension, not just more columns. For example, Passport - Person, Engine - Car, MedicalRecord - Patient, etc. They are separate concepts, even if tightly linked.
    • One-to-Many relationship: a single record in one model can be associated with multiple records in another model.

      For example, a teacher is qualified to teach a subject, but there can be more than one teacher in a college who teaches the same subject.

      class Teacher(models.Model):
        name = models.CharField(max_length=50)
        email = models.EmailField(max_length=50)
      
      
      class Subject(models.Model):
        teacher = model.ForeignKey(Teacher, on_delete=models.CASCADE, related_name="subjects")
        name = models.CharField(max_length=30)
        credits = model.IntegerField()
    • Many-to-Many relationship: multiple records in one model are associated with multiple records in another model.

      For example, more than one teacher can teach the same subject, and a single teacher can teach more than one subject.

      class Teacher(models.Model):
        name = models.CharField(max_length=50)
        email = models.EmailField(max_length=50)
      
      
      class Subject(models.Model):
        teacher = model.ManyToManyField(Teacher)
        name = models.CharField(max_length=30)
        credits = model.IntegerField()

      Note: Relational databases do not natively support Many‑to‑Many relationships and therefore require an intermediate table. Django generates this junction model automatically, managing referential integrity as well as admin and ORM integration. This simplifies development and prevents accidental duplicates, although it also reduces the level of customization available.

  • on_delete option sepcifies the behavior in case the associated object in the primary model is deleted. The values are:

    • CASCADE: deletes the object containing the ForeignKey. Deleting the reference object will also delete the referred objects.

      For example, suppose that a vehicle belongs to a customer. When the customer is deleted, all the vehicles that reference the customer will be automatically deleted.

    • PROTECT: is the opposite of CASCADE. It prevents deletion of a referenced object if it has an object referencing it in the database.

      For example, if a customer has vehicles, it cannot be deleted. Django will raise the ProtectedError if the customer is forcefully deleted.

    • RESTRICT: prevents deletion of the referenced object by raising RestrictedError, but it allows deletion if all referencing objects are also being deleted in the same operation.

      This behavior is different from PROTECT, which prevents deletion whenever a related reference exists, even when that referenced record is being deleted too.

      For example, with the code block below

      # create a principal associated with the college with id 1
      college = College.objects.get(pk=1)
      pricipal = Principal.objects.create(
        college=college, qualification="good", email="principal@college.com"
      )
      principal.save()
      
      # delete both the college and the associated principal
      college.delete()
      principal.delete()
      • if the relationship between College and Principal is set to PROTECT, the deletion will be blocked and a ProtectedError will be raised.
      • if the relationship between College and Principal is set RESTRICT, the deletion will be succeed because Django recognizes that the referenced object will be deleted as part of the same operation.
    • Note: when delete methods are called, Django does NOT delete rows immediately. Instead, it first plans the entire delete, then checks whether it's legal, then executes it.

      • Builds a delete graph.
      • Checks constraints (on_delete).
      • Executes deletes in a safe order.

      Under PROTECT relationship, Django aborts the deletion immediately as soon as it detects a related object. It does not considers whether that the related object might also be deleted later, no delete graph analysis is performed.

      Under RESTRICT relationship, Django waits until the entire delete graph is known, then it determines whether any restricted objects would remain after the operation. If so, it raises RestrictedError; if not, the deletion procceds.

Migrations

  • Migration is a mechanism that translates the model changes into database schema changes, allowing for version control of the database structure.
  • It propagates any changes in the model structure such as adding, modifying, or removing a field attribute of a model class to the mapped table.
  • Django's migration is a version control system. It has the following commands:
    • makemigrations creates migration scripts that reflect changes made to models, which are then applied to the database.
    • migrate applies the migration scripts to the database, creating or modifying tables as defined in the migration files.
    • sqlmigrate shows the SQL query or queries executed when a certain migration script is run.
    • showmigrations displays the status of migrations, indicating which have been applied and which are pending.
  • When migrating a model, Django automatically names the table as [app_name]_[model_name], for instance, my_app_college, my_app_principal, etc. We can override this by assigning the desired name to db_table parameter of the Meta class, to be declared inside the model class, as shown below.
from django.db import models


  class College(models.Model):
    name = models.CharField(max_length=50)
    strength = models.IntegerField()
    website = models.URLField()

    class Meta:
      db_table = "college_info"

Django ORM (Object Relationship Mapping)

Object-Relational Mapping (ORM) is the ability to create a SQL query using object-oriented programming language. This enables a quick turnaround time in fast production environments that need constant updates.

Django has its own ORM layer. Its migration mechanism propagates the models in database tables. We need to construct a QuerySet via a Manager of a model class to retrieve objects from our database.

Each model is a Python class that subclasses django.db.models.Model. For example:

from django.db import models


class Menu(models.Model):
  name = models.CharField(max_length=100)
  cuisine = models.CharField(max_length=100)
  price = models.IntegerField()

Manager

  • Manager is the interface through which database queries are made for a model.

  • Every Django model has at least one Manager.

  • The default manager is objects.

  • A Manager method returns QuerySet object(s).

    For example:

    class Menu(models.Model):
      name = models.CharField(max_length=100)
      cuisine = models.CharField(max_length=100)
      price = models.IntegerField()
    
    print(type(Menu.objects))  # django.db.models.manager.Manager
    print(type(Menu.objects.all()))  # django.db.models.query.QuerySet
  • When to use Managers ?

    • Queries related to the entire table.
    • Default filtering logic.
    • Entry point for custom query methods.

QuerySet

  • A QuerySet is a lazy collection of objects retrieved from the database.

  • A QuerySet:

    • represents a database query.

    • can be filtered, sliced, ordered.

      For example:

      qs = Menu.objects.filter(name__icontains="pasta")
    • can be chained.

      For example:

      qs = Menu.objects.filter(name__icontains="pasta").order_by("name")

      These above query sets are chained to one SQL query, not many.

    • is a lazy evaluation (not executed until needed).

    • returns model instances.

    The query executes only when:

    • iterated over
    • converted to list
    • printed
    • accessed with the len function.
  • How to find the SQL query generated? theQuerySet's query attribute returns the SQL query generated.

    For example:

    qs = Menu.objects.filter(name__icontains="pasta").order_by("name").only("name", "price")
    print(qs.query)
    
    # SELECT "demo_app_menu"."id", "demo_app_menu"."name", "demo_app_menu"."price" FROM "demo_app_menu" WHERE "demo_app_menu"."name" LIKE %pasta% ESCAPE '\' ORDER BY "demo_app_menu"."name" ASC
  • In bef,

    • Manager: the entry point to datbase queries, lives on the model class.
    • QuerySet: the lazy, chainable representation of a datbase query.

CRUD Operations

  • Create a row in the table:

    • create an object of the model class then use the save method to creates a row in the table. For example:

      m = Menu(name="pho", cuisine="vietnam", price=12)
      m.save()
    • use the create method of the Manager. This method will return an instance of the model class.

      Menu.objects.create(name="pho", cuisine="vietnam", price=12)
  • Read rows:

    • fetch all objects by using the all method of the Manager. For example:

      Menu.objects.all()
    • apply filters to the data fetched from the model by using filter method of the Manager.

      Menu.objects.filter(name__startswith="p")
  • Update a row: get the object of that row, assign a new value to the attribute and save the object. For example:

    m = Menu.objects.get(pk=2)
    m.cuisine = "chinese"
    m.save()
  • Delete a row: get the object of the corresponding row then call the delete method. For example:

    m = Menu.objects.get(pk=4)
    m.delete()

Common Problems

The Author and Book classes below are used to demonstrate the problems.

from django.db import models


class Author(models.Model):
  name = models.CharField(max_length=100)


class Book(models.Model):
  author = models.ForeignKey(Author, on_delete=models.CASCADE)
  title = models.CharField(max_length=100)
  status = models.CharField(max_length=50)

N+1 Problem

Refers to a problem where one query is used to fetch a list of objects, followed by N additional queries to fetch their related data. For example:

books = Book.objects.all()

for book in books:
  print(book.author.name)

To fix the problem, we need to use select_related (for One-to-One or One-to-Many relationships) or prefetch_related (for Many-to-Many or reverse relationships). For example:

books = Book.objects.select_related("author")

for book in books:
  print(book.author.name)

Sequential Scan (Seq Scan) Problem

Occurs the database reads every row in a table, one by one, to find those that match the query. For example:

Book.objects.filter(status="BORROWED")

To fix the problem, we can add indexes to the queried fields. For example:

class Book(models.Model):
  status = models.CharField(max_length=50, db_index=True)

Fetching Too Many Columns Problem

Refers to a data-access performance problem where the query retrieves more fields than the actual needs. For example:

Book.objects.all()

To fix the problem, we can use the only method to specify the fields we need. For example:

Book.objects.only("id", "status")

Too many JOINs Problem

Happens when a query joins more tables than are actually needed, making it slow, complex, and hard for the database optimizer to execute efficiently. For example:

Book.objects.select_related("author__profile__country__continent")

To fix the problem, we should join only what we need, fetch specific fields rather than entire tables, or intentionally split queries. For example:

# join only what we need
Book.objects.select_related("author")

# fetch specific fields
Book.objects.only("id", "author__name")

Django Form

  • A form is a document wherein the user enters their responses at certain labeled placeholders.

  • Django includes a Form class, its attributes, and methods in the django.forms module. This class is used as a base for a user-defined form design.

    form django import forms
    
    
    class ApplicationForm(forms.Form):
      pass
  • The attributes of the form are Field class objects. The django.forms module has a collection of Field types. These fields correspond to the HTML elements they enventually render on the user's browser. For example:

    • the forms.CharField is translated to HTML's text input type.

    • the forms.ChoiceField is equivalent to <select> in HTML.

      from django import forms
      
      
      POSTS = (
        ("mananger", "Manager"),
        ("cashier", "Cashier"),
        ("operator", "Operator"),
      )
      
      
      class ApplicationForm(forms.Form):
        name = forms.CharField(label="Name of Application", max_length=50)
        address = forms.CharField(label="Address", max_length=100)
        post = forms.ChoiceField(choices=POSTS)
  • By convention, the user-defined form classes are stored in a forms.py file in the app's package folder.

Form Fields

Some of the most frequently used fields are as follow:

  • CharField: translates to input type=text HTML form element.

    forms.CharField(label="Name of Application", max_length=50)

    Set the field's widget property to forms.Textarea to create a textarea.

    forms.CharField(label="Name of Application", widget=forms.Textarea)
  • EmailField: a CharField that can validate if the text entered is a valid email.

    forms.EmailField(max_length=254)
  • IntegerField: similar to a CharField but customized to accept only integer numbers. We can limit the value entered by setting min_value and max_value parameters.

    forms.IntegerField(min_value=0, max_value=10)
  • FloatField: a text input field that validates if the input is a valid float number.

    forms.FloatField()
  • DecimalField: similar to FloatField but supports fixed numbers of decimal places.

    forms.DecimalField(max_digits=10, decimal_places=2)

    max_digits and decimal_places are mandatory parameters of this field type.

  • FileField: presents an input type=file element on the HTML form.

    forms.FileField(upload_to="uploads/")
  • ImageField: similar to FileField with added validation to check if the uploaded file is an image. The pillow library is required for this field type to be used.

    forms.ImageField(upload_to="uploads/")
  • ChoiceField: emulates the HTML's select element. Populates the drop-down list with a choices parameter whose value should be a sequence of two item tuples ([value], [text_displayed]).

    PAYMENT_CHOICES = (
      ("card", "Credit Card"),
      ("cash", "Cash"),
      ("upi", "UPI"),
    )
    
    forms.ChoiceField(choices=PAYMENT_CHOICES)

    Set the field's widget property to forms.RadioSelect to create radio buttons.

    forms.ChoiceField(choices=PAYMENT_CHOICES, widget=forms.RadioSelect)

Meta Class

  • In Django Form and ModelForm classes, Meta class is an inner configuration class. It tells Django how the form should be constructed. For example:

    from django import forms
    from restaurant.models import Dish
    
    
    class DishForm(forms.ModelForm):
      class Meta:
        model = Dish
        fields = ["name", "description", "price"]
  • The Meta class instruct Django to:

    • read the model,
    • auto-generate fields,
    • reuse validators, and
    • keeps form and model in sync.

    This reduces duplication and helps prevent bugs.

  • For non-Model forms, Meta is optional and rarely needed. For example:

    from django import forms
    
    
    class ContactForm(forms.Form):
      email = forms.EmailField()
    
      class Meta:
        fields = ["email"]
  • Common Meta options include:

    • model: specifies which model the form is based on. For example:

      model = Dish
    • fields: lists the model fields to include. Prefer fields whenever possible (safer and explicit). For example:

      fields = ["name", "description", "price"]
    • exclude: lists model fields to exclude. For example:

      excludes = ["created_at"]
    • widgets: customizes HTML widgets and controls HTML rendering. For example:

      widgets = {
        "price": forms.NumberInput(attrs={"step": "0.1"}),
      }
    • labels: defines human-readable field labels. For example:

      labels = {
        "price": "Price (€)",
      }
    • help_texts: provides help text for templates. For example:

      labels = {
        "price": "Enter price in Euro",
      }
    • error_messages: customizes validation error messages. For example:

      error_messages = {
        "name": {
          "required": "Please enter a dish name.",
          "unique": "This dish already exists."
        },
      }
  • Model field validators are automatically applied in this order:

    1. Field-level validation: required fields (blank=False), type checks (e.g., DecimalField, CharField, etc.), and built-in validators (e.g., MinValueValidator, MaxValueValidator, etc.).
    2. clean_[field]() methods.
    3. clean() for form-wide validation.
    4. Model-level validation: unique=True, unique_together, constraints, etc.
  • We can still override or extend validation using clean_[field]() or clean() methods.

  • Meta is required for ModelForm, optional and usually ignored for Form.

  • The Meta should contain configuration, not business logic.

Form Rendering

To render a form object on a browser, we have to first write an HTML template and put the form object in jinja2 tag. For example, we create the form.html file as follow:

<html>
  <body>
    <form action="{% url 'application-form' %}" action="POST">
      {% csrf_token %}
      {{ form }}
      <input type="submit" value="Submit">
    </form>
  </body>
</html>

Then in the app's views.py file which renders the form.html template and sends the ApplicationForm object as a context.

from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from .forms import ApplicationForm


def application_form(request: HttpRequest) -> HttpResponse:
  form = ApplicationForm()
  return render(request, "application_form.html", {"form": form})

Inside the form.html template, the form can be rendered in different ways:

  • {{ form.as_div }}: renders the form as divisions wrapped in <div> tags. For example:

    <div>
      <label for="id_name">Name of Applicant:</label>
      <input type="text" name="name" maxlength="50" required id="id_name">
    </div>

    The form is rendered as a div by default.

  • {{ form.as_table }}: renders the form as table cells wrapped in <tr> tags. For example:

    <tr>
      <th><label for="id_name">Name of Applicant:</label></th>
      <td>
        <input type="text" name="name" maxlength="50" required id="id_name">
      </td>
    </tr>
  • {{ form.as_p }}: renders the form as paragraphs wrapped in <p> tags. For example:

    <p>
      <label for="id_name">Name of Applicant:</label>
      <input type="text" name="name" maxlength="50" required id="id_name">
    </p>
  • {{ form.as_ul }}: renders the form as list items wrapped in <li> tags. For example:

    <li>
      <label for="id_name">Name of Applicant:</label>
      <input type="text" name="name" maxlength="50" required id="id_name">
    </li>

Reading From Contents

  • A view function processes data submitted by the user, whether to create a new record or perform other server‑side actions.

  • It first populate the form object with the POST data and check it is valid.

  • The django.forms.Form class provides the is_valid method to run validation on each field and return True if all field validations are passed. For example:

    from django.http import HttpRequest, HttpResponse
    from django.shortcuts import render
    from .forms import ApplicationForm
    
    
    def application_form(request: HttpRequest) -> HttpResponse:
      form = ApplicationForm()
    
      if request.method == "POST":
        form = ApplicationForm(request.POST)
        if form.is_valid():
          form.save()  # save the data submitted to the datbase
          return HttpResponse("data submitted is processed!")
    
        # data submitted is invalid
        return HttpResponse("data submitted is invalid!")
    
      # render the application form
      return render(request, "application_form.html", {"form": form})
  • Once the Form instance is validated, we can access the data individual field via its cleaned_data attribute. It ensures that the field contains the output in consistent form. For example:

    name = form.cleaned_data["name"]
    address = form.cleaned_data["address"]
    post = form.cleaned_data["post"]

Cross-Site Request Forgery (CSRF) Attack

  • CSRF (Cross-Site Request Forgery) exploits the browser's implicit trust, not a bug in the code.

    Browsers automatically send cookies (session, auth) with every request to a domain, even if the request was triggered by another site.

  • A CSRF attack tricks a logged-in user's browser into sending an unintended request to a trusted site, using the user's own cookies to perform an action without consent. For example:

    • The user logs into bank.com.
    • The browser stores a session cookie session_id=abc123.
    • The user visits evil.com while still logged in bank.com.
    • evil.com contains a hidden HTML form to send a request to bank.com.
    • The browser automatically includes user's session cookie.
    • The bank server sees valid session cookie, valid POST request then it processes the request.

    The scenario works because cookies are included automatically and the server cannot tell whether the request came from the legitimate form or from another site. That's the CSRF vulnerability.

    Cookies alone are not enough, because they prove who the user is but not where the request comes from.

  • How CSRF tokens work?

    • When the user visits the website, Django generates a radom, secret CSRF token for the user.
    • The token is sent and stored in a cookie in the browser.
    • When the user submits a form, the token stored in the cookies is embedded into the HTML form as a hidden field generated by {% csrf_token %}.
    • The browser sends both cookies and the form token.
    • Django's CSRF middleware compares the two tokens.
    • If they match, the request is accepted.
    • If missing or different, Django returns 403 Forbidden.

    The attacker cannot supply the correct CSRF token because:

    • it cannot read cookies from another domain.
    • it cannot read form HTML from another domain.
    • it cannot guess token (random and long).
  • When to use a CSRF token and when not to?

    • CSRF token required for POST, PUT, PATCH, DELETE when using forms or modifying server-side data.
    • CSRF token not required for GET requests and read-only views.

Django Admin

  • Django Admin Interface, usually called Django Admin is a built-in web application that allows for easy management of users, groups, and permissions.

  • To access the Django Admin, a superuser is required. A superuser has the privileges to add or modify users and groups, and can be created using the createsuperuser command as follows.

    > python manage.py createsuperuser
  • If a user's is_staff property is set to True, they can log in to the admin interface. Non-staff users cannot access the admin site.

  • Django's admin site provides a very easy-to-use interface to add and modify users and groups, there are no real restrictions as the User admin. For instance, a user with staff status:

    • can manage the other users.
    • can edit their own permissions, which is not warranted.
    • can allocate superuser right.

    The out-of-box implementation of the admin site doesn't prevent this.

  • Since Django Admin is restricted to staff users, to allow regular (non-staff) users to log in the website, we need to create a login page using Django's built-in authentication system, as shown in the example below.

    # views.py
    from django.contrib.auth import authentication, login
    from django.http import HttpRequest, HttpResponse
    from django.shortcuts import render, redirect
    
    
    def user_login(request: HttpRequest) -> HttpResponse:
      if request.method != "POST":
        return render(request, "login.html")
    
      username = request.POST.get("username")
      password = request.POST.get("password")
      user = authenticate(request, username=username, password=password)
    
      if user is None:
        return render(request, "login.html", {"error": "Invalid credentials!"})
    
      login(request, user)  # work for non-staff users
      return redirect("home")

Customizing User Admin

  • The UserAdmin class from the django.contrib.auth.admin module allows developers to control which fields are editable and to implement additional security measures.

  • To customize the User Admin, we first extend the UserAdmin class in the app's admin.py file, then unregister the default User model and register it with the new exteded class.

    # admin.py
    from django.contrib import admin
    from django.contrib.auth.models import User
    from django.contrib.auth.admin import UserAdmin
    
    
    class CustomUserAdmin(UserAdmin):
      pass
    
    
    admin.site.unregister(User)
    admin.site.register(User, CustomUserAdmin)

    Alternatively, we can unregister the User model first, then use the admin.register decorator to create and register it with the extended class, as shown below.

    # admin.py
    from django.contrib import admin
    from django.contrib.auth.models import User
    from django.contrib.auth.admin import UserAdmin
    
    admin.site.unregister(User)
    
    
    @admin.register(User)
    class CustomUserAdmin(UserAdmin):
      pass
  • In the example below, the CustomUserAdmin class prevents users from modifying the last_login and date_joined fields. It also prevents non-superusers from changing the username, assigning groups, user permissions, or granting superuser privileges by marking corresponding fields as read-only.

    class CustomUserAdmin(UserAdmin):
      readonly_fields: tuple[str, ...] = ("last_login", "date_joined")
    
      def get_readonly_fields(self, request: HttpRequest, obj: Any = None) -> tuple[str, ...]:
        if obj and not request.user.is_superuser:
          return self.readonly_fields + (
            "username",
            "groups",
            "user_permissions",
            "is_superuser",
          )
        return self.readonly_fields
  • Notes:

    • If two apps both customize UserAdmin, the one registered last wins, and the other is silently overridden. No error. No warning. Just override.

      An app's loading order is determined by its position in the INSTALLED_APPS list. For example:

      # settings.py
      INSTALLED_APPS = [
        "app_loaded_first",
        "app_loaded_second",
      ]
    • Best practice: create one app responsible for the User Admin.

Customizing Model Admin

  • Users can perform CRUD operations on a model through the Django Admin. To enable this, the model must be registered with the admin site as follows.

    • define the model in the app's models.py file. For example:

      # models.py
      from django.db import models
      
      
      class Book(models.Model):
        title = models.CharField(max_length=100)
        author = models.CharField(max_length=100)
    • register the model with the admin site in the app's admin.py file. For example:

      # admin.py
      from django.contrib import admin
      from .models import Book
      
      
      admin.site.register(Book)
  • To customize a model's admin interface, we first create a custom admin class by extending the ModelAdmin class, imported from django.contrib.admin module, then register the model with this new admin class, similar to how we customize the User Admin. For example:

    from django.contrib import admin
    
    class BookAdmin(admin.ModelAdmin):
      list_display = ("title", "author")
      search_fields = ("title__contains", )
    
    
    admin.site.register(Book, BookAdmin)

Permissions

  • Django has an in-built system for handling permissions. This authentication system has features for both authentication and authorization.

  • A user in Django can be one of three classifications:

    • superuser: is a top level user or adminstrator of the system. This type of user possesses permission to add, change, or delete other users, as well as perform operations on all the data in the project.
    • staff user: is allowed to access Django Admin Interface. However, a staff user doesn't automatically get the permission to create, read, update, and delete data in the Django admin. It must be given explicitly.
    • (regular) user: is not authorized to use the admin site. When a user is created, they're marked as a regular, active user by default.
  • Setting the is_staff and is_superuser properties to True makes a user a staff user or a superuser, respectively.

  • The permission mechanism is handled by the django.contrib.auth app.

  • When a model is created, Django automatically creates add, change, delete, and view permissions. These permissions follow the naming pattern [app].[action_model] pattern.

    • app: is the application name.
    • action: is add, change, delete, or view.
    • model: is the model name in lowercase.

    For instance, my_app.add_mymodel represents the permission required to add a MyModel instance in the my_app application.

  • A Django group is a convient way to assign the same set of permissions to multiple users. A group is simply a collection of permissions that can be applied to one or more users.

Enforcing Permissions

  • Django app receives user information through the request context.

  • Permissions are often enforced at the view layer. However, they can also be applied within templates, URL configurations, and both function-based and class-based views.

  • Enforcing permissions in views:

    Below are several common ways to verify that a user is logged in and authenticated:

    • Use the is_anonymous function of the request.user object. For example:

      from django.core.exceptions import PermissionDenied
      from django.http import HttpRequest, HttpResponse
      
      
      def my_view(request: HttpRequest) -> HttpResponse:
        if request.user.is_anonymous():
          raise PermisionDenied()
        return HttpResponse("Authenticated user!")
    • Use the login_required decorator, imported from django.contrib.auth.decorators module. For example:

      from django.contrib.auth.decorators import login_required
      from django.http import HttpRequest, HttpResponse
      
      
      @login_required
      def my_view(request: HttpRequest) -> HttpResponse
        return HttpResponse("Authenticated user!")
    • Use the user_passes_test decorator, imported from django.contrib.auth.decorators module. This decorator takes a single required argument, which is a boolean function, so it can be used for both authentication and authorization. For example:

      from typing import Union
      
      from django.contrib.auth.base_user import AbstractBaseUser
      from django.contrib.auth.decorators import user_passes_test
      from django.contrib.auth.models import AnonymousUser
      from django.http import HttpRequest, HttpResponse
      
      
      def verify_permission(user: Union[AbstractBaseUser, AnonymousUser]) -> bool:
        return user.is_authenticated() and user.has_perm("my_app.change_category")
      
      
      @user_passes_test(verify_permission)
      def update_category(request: HttpRequest) -> HttpResponse
        return HttpResponse("Authorized user!")

    For authorization:

    • Use the permission_required decorator, imported from django.contrib.auth.decorators with function-based views. For example:

      from django.contrib.auth.decorators import permission_required
      from django.http import HttpRequest, HttpResponse
      
      
      @permission_required("my_app.change_category")
      def update_category(request: HttpRequest) -> HttpResponse
        return HttpResponse("Authorized user!")
    • Use the PermissionRequiredMixin class, imported from django.contrib.auth.mixins with class-based views. For example:

      from django.contrib.auth.mixins import PermissionRequiredMixin
      from django.views.generic import ListView
      
      from .models import Product
      
      
      class ProductListView(PermissionRequiredMixin, ListView):
        model = Product
        permission_required = "my_app.view_product"
        template_name = "product.html"
  • Enforcing permissions in templates:

    • Django automatically injects the user and perms variables into templates, making authentication and authorization checks available directly in the template context.

    • Check the authentication:

      {% if user.is_authenticated %}
      Authenticated user!
      {% endif %}
    • Check the authorization:

      {% if perms.my_app.view_product %}
      Authorized user!
      {% endif %}
    • Note that templates do not enforce security on their own. They only hide UI elements and improve the UX. Actual permission enforcement must be handled on the server side, typically within views.

  • Enforcing permissions in URL patterns:

    • URL patterns cannot enforce permissions on their own, but we can wrap views with permission checks in urls.py to enforce access control at the URL level. For example:

      # urls.py
      from django.contrib.auth.decorators import login_required, permission_required
      from django.urls import path
      
      from . import views
      
      urlpatterns = [
        path("products/", login_required(views.display_products), name="view_product"),
        path(
          "products/<int:pk>/edit/",
          permission_required("my_app.change_product")(views.update_product),
          name="update_product"
        ),
      ]
    • This approach to enforcing permissions is not recommended.

    • Best practice: URLs route requests. Views enforce permissions.

Database Configuration

  • By default, Django uses the SQLite database for storing and retrieving application data, since Python provides built-in support for it.
  • Django also supports other databases such as PostgreSQL, MySQL, and more.

Setup Steps

The following steps outline how to configure Django with supported databases.

  • Install the database server. For example: PostgreSQL, MySQL.

  • Create a database and user, ensuring both are ready to use.

  • Install the appropriate Python driver so Django can connect to the database. For example:

    # PostgreSQL driver
    > pip install psycopg2-binary
    
    # MySQL driver
    > pip install mysqlclient
  • Configure settings.py by updating the DATABASES setting with the correct ENGINE, NAME, USER, PASSWORD, HOST, PORT, and any required OPTIONS. For example:

    # PostgreSQL
    DATABASES = {
      "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "mydb",
        "USER": "myuser",
        "PASSWORD": "mypassword",
        "HOST": "localhost",
        "PORT": "5432",
      }
    }
    
    # MySQL
    DATABASES = {
      "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": "mydb",
        "USER": "myuser",
        "PASSWORD": "mypassword",
        "HOST": "localhost",
        "PORT": "3306",
        "OPTIONS": {
          "init_command": "SET sql_mode='STRICT_TRANS_TABLES'",
        },
      }
    }
  • Run migrations to initialize the database schema.

    > python manage.py makemigrations
    > python manage.py migrate
  • Test connection to confirm everything is working correctly.

    > python manage.py dbshell

    If the command opens a DB shell (psql, mysql, etc.), the config works.

Environment Variables vs. Configuration Files

  • Best practice:

    • Never hardcode sensitive information in source code, as it can easily be exposed, leaked, or commited to version control.
    • Use environment variables to manage secrets and deployment-specific values.
    • Use configuration files to define structure, defaults, and non-sensitive settings.
  • python-decouple is a lightweight Python package that helps separate configuration from code, especially secrets and environment-specific settings. It allows Python read configuration from environment variables and .env files in a clean and safe way. It:

    • reads environment variables first,
    • falls back to a .env file (for local development),
    • converts values to proper Python types, and
    • keeps secrets out of source code.

    For example:

    from decouple import config
    
    
    DEBUG = config("DEBUG", default=False, cast=bool)
    NAME = config("DB_NAME")
    USER = config("DB_USER")
    PASSWORD = config("DB_PASSWORD")
    HOST = config("HOST")

Templates

  • The view functions retrieve the data from the database connected to the application, and Django uses templates and the Django Template Language (DTL) to display this dynamic data.

  • Templates form the presentation layer in the MVT architecture.

  • Templates consist mainly two types of content:

    • static: the HTML that does not change on the web page. It defines the structure and layout of the page.
    • template language: the syntax that allows to insert dynamic data.

    For example:

    <h2>This weeks special is {{ dish_name }} with a price of {{ price }}.</h2>
  • Dynamic data is rendered by the render function, imported from django.shortcuts in view functions.

  • The render function takes three parameters:

    • a request object: represents the initial HTTP request to object.
    • a template path: represents the relative path of the HTML file to the template directory.
    • and a dictionary of variables: provides context data for the template. During rendering, template variables are replaced with their corresponding values from the context.

    For example:

    # views.py
    from django.http import HttpRequest, HttpResponse
    from django.shortcuts import render
    
    def dish_of_the_day(request: HttpRequest) -> HttpResponse:
      context = {"dish_name": "pasta", "price": 10.5}
      return render(request, "dish_of_the_day.html", context)

Django Template Language (DTL)

  • Django Template Language (DTL) is a built-in templating system to generate dynamic HTML pages by combining static HTML with dynamic data from Django views.
  • DTL consists of constructs such as:
    • variables: used to display data passed from the view to the template. For example:

      <p>Username: {{ user.username }}</p>
      <p>Age: {{ age }}</p>
    • tags: used to add logic like conditions, loops, and template control. For example:

      {% if user.is_authenticated %}
      <p>Welcome back!</p>
      {% endif %}
      
      {% for item in menu_items %}
      <li>{{ item }}</li>
      {% endfor %}
    • filters: used to modify how variables are displayed. For example:

      <p>{{ name|upper }}</p> <!-- converts to uppercase -->
      <p>{{ description|truncatewords:5 }}</p> <!-- limit words -->
      <p>{{ price|floatformat:2 }}</p> <!-- format decimal -->
      <p>{{ text|length }}</p> <!-- returns length -->
    • comments: used to write notes that will not appear in the rendered HTML. For example:

      {# This is a single-line comment #}
      
      {% comment %}
      This is a multi-line comment.
      It will not shown in the output.
      {% endcomment %}

TEMPLATES Settings

  • Django uses template loaders to locate templates based on the configuration defined in the TEMPLATES section in the project's settings.py file.

  • TEMPLATES is a list of template engine configurations. Django allows multiple template engines, but most projects use Django Template Language (DTL). For example:

    TEMPLATES = [
      {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
          'context_processors': [
            'django.template.context_processors.request',
            'django.contrib.auth.context_processors.auth',
            'django.contrib.messages.context_processors.messages',
          ],
        },
      },
    ]
  • BACKEND: specifies which template engine Django should use to interpret templates. Using django.template.backends.django.DjangoTemplates means the Django Template Language is in use.

  • DIRS: a list of absolute paths where Django will look for templates. Common usage:

    'DIRS': [BASE_DIR / "templates"],

    This configuration allows Django to look for templates in a project-level templates folder.

  • APP_DIRS: indicates whether Django should search for templates inside installed apps.

    • True: Django looks for templates in a templates folder within each installed app. The folder name templates is fixed unless custom template loaders are configured.
    • False: Django does not search for templates inside app directories.
  • How Django finds a template?

    • It first searches the directories listed in DIRS.
    • Then it looks in the templates/ folders of installed apps (if APP_DIRS is set to True).
    • Next, it applies context processors.
    • Finally, it renders the HTML.

    Note: Django uses the first matching template it finds which is why project-level templates can override templates provided by installed apps.

  • Best practice:

    • Use the directories listed in DIRS for shared or base templates.

    • Use each installed apps's templates directory for app-specific templates.

    • Place an app's templates inside templates/[app_name]/[template_name] to avoid accidental overrides. For example:

      my_app/
      └── templates/
          └── my_app/
              └── home.html
      
  • OPTIONS: provides extra configuration for the template engine.

  • context_processors: automatically add variables to every template context.

    • django.template.context_processors.request: adds the request object to templates.
    • django.contrib.auth.context_processors.auth: adds authentication-related variables user, perms.
    • django.contrib.messages.context_processors.messages: adds Django messages framework support. It is used for success messages, error notifications, alerts.

Template Inheritance

  • Template inheritance lets us to define a base template containing common elements such as the header, footer, and navigation, and then extend it in other templates.

  • Instead of duplicating HTML, child templates simply fill in the blocks defined in the base template.

  • Base template:

    • contains the common structure of the site.
    • defines blocks that child templates can override.

    For example: base.html

    <!DOCTYPE html>
    <html>
    <head>
      <title>{% block title %}My Website{% endblock %}</title>
    </head>
    <body>
      <header>
        <h1>My Website Header</h1>
      </header>
    
      <nav>
        <a href="/">Home</a>
        <a href="/about/">About</a>
      </nav>
    
      <main>
        {% block content %}
        <!-- Child templates will inject content here -->
        {% endblock %}
      </main>
    
      <footer>
        <p>© 2026 My Website</p>
      </footer>
    </body>
    </html>
  • Child templates:

    • use {% extend %} to inherit from a base template.
    • override blocks defined in the base template.

    For example: home.html

    {% extend "base.html" %}
    
    {% block title %}
    Home - My Website
    {% endblock %}
    
    {% block content %}
    <h2>Welcome to the home page!</h2>
    <p>This is some dynamic content.</p>
    {% endblock %}
  • How it works?

    1. Django loads the child template.
    2. It encounters the {% extends %} tag and then loads the base template.
    3. It replace the base template's {% block %} sections with the content provided by the child template.
    4. The result is a complete HTML page that combines the base layout with the child template's content.

Extends vs. Include Tags

  • Both {% extends %} and {% include %} support template reuse, but they serve very different purposes.

  • The {% extends %} tag enables template inheritance. It:

    • establishes a parent-child relationship between templates.
    • defines the overall page layout by referencing a base template.
    • allows the child template to replace the blocks defined in the parent.
  • Key characteristics of the {% extends %} tag:

    • only one {% extends %} tag is allowed per template.
    • it must be the first tag in the template.
    • it works with {% block %} tags to enable template inheritance.
  • The {% include %} tag enables template composition. It:

    • inserts another template into the current template.
    • does not involve inheritance or block overriding.
    • simply renders the included templace in place.
  • Key characteristics of the {% include %} tag:

    • it can be used multiple times within a template.
    • it can appear anywhere in the template structure.
    • it can receive variables passed from the parent template.

    For example:

    <!-- head.html -->
    <head>
      <title>{{ title }}</title>
    </head>
    
    
    <!-- footer.html -->
    <footer>
      <p>© 2026 My Website</p>
    </footer>
    
    
    <!-- base.html -->
    <html>
      {% include "head.html" with title="Home" %}
      <body>
        {% block content %}
        {% endblock %}
        {% include "footer.html" %}
      </body>
    </html>
  • In practice, use the {% extends %} tag when we need a full page layout, and use the {% include %} tag when we want small, reusable components.

  • Best practice for combining layout and components:

    templates/
    └── my_app/
        ├── base.html
        ├── home.html
        ├── about.html
        └── includes/
            ├── navbar.html
            └── footer.html
    

Static Files

  • A web application needs to serve static content to the client such as images, fonts, Javascript files, and style sheet. These assets are managed by the django.contrib.staticfiles app, which handles locating, collecting, and serving static files in a consistent and organized way.

  • When rendering a static asset (for instance, an image stored in a static folder) we must use the {% static %} tag to generate the correct URL. For example:

    {% load static %}
    <img src="{% static 'my_app/images/logo.png' %}" alt="Logo">
  • The {% load static %} tag loads the {% static %} tag into the template and should be placed at the top of temlates. For example:

    <!-- base.html -->
    {% load static %}
    <!DOCTYPE html>
    <html>
    <head>
      <link rel="stylesheet" href="{% static 'my_app/css/style.css' %}">
      <script src="{% static 'my_app/js/main.js' %}"></script>
      <link rel="icon" href="{% static 'my_app/images/favicon.ico' %}">
      <title>{% block title %}My Website{% endblock %}</title>
    </head>
    <body>
      {% block content %}
      {% endblock %}
    </body>
    </html>
  • The STATIC_URL setting in settings.py defines the URL prefix used to serve static files. Templates use this prefix through the {% static %} tag.

    For example, with STATIC_URL = 'static/' the tag {% static 'css/style.css' %} generates the URL /static/css/style.css.

    However, STATIC_URL does not tell Django where static files are stored. It only defines how they are acessed via a URL.

  • The STATICFILES_DIRS setting in settings.py tells Django where to look for additional static files. It is mainly used for project-level satic assets. A common configuration is:

    STATICFILES_DIRS = [
      BASE_DIR / "static"
    ]

    Django searches static files in the following order:

    1. app-level static/ directories (inside each installed app).
    2. directories listed in STATICFILES_DIR.

    STATICFILES_DIR is not set by default because Django prioritizes app-level static files. We should define STATICFILES_DIR when we:

    • neeed project-level static files that are not tied to any specific app.
    • have shared assets used across multiple apps, e.g., global CSS, JS, or images.
  • Best practice, always namespace static files to prevent filename collisions and keep assets organized by type. For example:

    my_app/
    └── static/
      └── my_app/
        └── css/
          └── style.css
        └── js/
          └── main.js
        └── images/
          ├── favicon.ico
          └── logo.png
    

Testing in Django

  • Django's testing system is built on Python's unittest framework, with additional Django-specific extensions.

  • Tests follow a class-based structure: each test class inherits from django.test.TestCase, and individual test methods begin with test_.

  • The test management command is used to run the test suite.

    python manage.py test

    When executed, Django will:

    • automatically discover all tests,
    • create a temporary test database,
    • run each test in isolation,
    • and delete the test database once the suite completes.

Unit Test Naming Conventions

  • Django projects typically use behavior-driven, feature-grouped naming. The goal is for tests to read like documentation of the app's behavior.

  • Group tests by feature using test classes.

    • Each Django app should contain a dedicated tests/ package. For example:

      my_app/
      └── tests/
        ├── __init__.py
        ├── test_models.py
        ├── test_views.py
        └── test_urls.py
      
    • Inside each file, group tests by domain concept, not by method name. For example:

      class TestDishModel(TestCase):
      """
      TestDishModel class
      """
      
      def test_string_representation_includes_name(self):
        """
        Verifies that the string representation includes the dish name
        """
      
      def test_string_representation_includes_price(self):
        """
        Verifies that the string representation includes the dish price
        """
  • Use the naming pattern test_[action]_[condition]_[expected]. This format expresses business rules clearly and avoids leaking implementation details. It is both Django-friendly and Pythonic style. For example:

    def test_home_url_resolves_to_correct_view(self):
      """
      Verifies that the home URL resolves to the correct view
      """
    
    def test_about_view_returns_200(self):
      """
      Verifies that the about view returns a 200 OK response
      """
    
    def test_form_is_invalid_with_incorrect_no_guests(self):
      """
      Verifies that the form fails validation when the number of guests is incorrect
      """
  • For views, use HTTP-focused naming. For example:

    def test_home_view_returns_200(self):
      """
      Verifies that the home view returns a 200 OK response
      """
    
    def test_about_view_returns_200(self):
      """
      Verifies that the about view returns a 200 OK response
      """
  • For models, focus on behavior, not fields. For example:

    def test_string_representation_includes_name(self):
      """
      Verifies that the string representation includes the dish name
      """
    
    def test_string_representation_includes_no_guests(self):
      """
      Verifies that the string representation includes number of guests
      """
  • For forms, emphasize validation rules. For example:

    def test_form_is_valid_with_correct_data(self):
      """
      Verifies that the form validates successfully with correct input data
      """
    
    def test_form_is_invalid_with_incorrect_first_name(self):
      """
      Verifies that the form fails validation when the first name is incorrect
      """

Writing Tests

  • Testing simple cases. For example:

    from django.test import TestCase
    
    
    class TestSimple(TestCase):
      def test_addition_returns_correct_value(self):
        """
        Verifies that the addition returns a correct value
        """
        self.assertEqual(1 + 1, 2)
  • Testing views. For example:

    from django.test import TestCase
    from django.urls import reverse
    
    
    def test_home_view_returns_200(self):
      """
      Verifies that the home view returns a 200 OK response
      """
      response = self.client.get(reverse("restaurant:home"))
      self.assertEqual(response.status_code, 200)
  • Testing models. For example:

    from django.test import TestCase
    
    from restaurant.models import Dish
    
    
    class TestDishModel(TestCase):
      """
      TestDishModel class
      """
    
      def test_string_representation_includes_name(self):
        """
        Verifies that the string representation includes the dish name
        """
        dish = Dish(name="Pizza", price=12.5)
        self.assertIn(dish.name, str(dish))
  • Testing forms. For example:

    from datetime import datetime, timedelta, timezone
    from unittest.mock import patch
    
    from django.test import TestCase
    
    from restaurant.forms import ReservationForm
    
    
    class TestReservationForm(TestCase):
      """
      TestReservationForm class
      """
    
      def setUp(self) -> None:
        """
        Setups attributes for ReservationForm tests
        """
        self.fake_now = datetime(2025, 1, 1, tzinfo=timezone.utc)
        self.reserved_at = self.fake_now + timedelta(days=1)
    
      def test_form_is_valid_with_correct_data(self):
        """
        Verifies that the form validates successfully with correct input data
        """
        with patch("restaurant.forms.timezone.now", return_value=self.fake_now):
          form = ReservationForm(
            data={
              "first_name": "John",
              "last_name": "Doe",
              "no_guests": 2,
              "reserved_at": self.reserved_at,
              "comment": "",
            }
          )
          self.assertEqual(form.is_valid(), True)
  • Testing templates. For example:

    from django.test import TestCase
    from django.urls import reverse
    
    
    class TestTemplate(TestCase):
      """
      TestTemplate class
      """
    
      def test_templates_used_by_home_url(self):
        """
        Verifies that the home url renders the expected template
        """
        response = self.client.get(reverse("restaurant:home"))
        self.assertTemplateUsed(response, "restaurant/home.html")
  • Testing urls. For example:

    from django.test import TestCase
    from django.urls import resolve, reverse
    
    from restaurant.views import HomeView
    
    
    class TestUrl(TestCase):
      """
      TestUrl class
      """
    
      def test_home_url_resolves_to_correct_view(self):
        """
        Verifies that the home URL resolves to the correct view
        """
        resolver = resolve(reverse("restaurant:home"))
        self.assertEqual(resolver.func.view_class, HomeView)
  • Testing with the database. For example:

    from django.test import TestCase
    
    from restaurant.models import Dish
    
    
    class TestDishModel(TestCase):
      """
      TestDishModel class
      """
    
      def test_creates_dish_when_data_is_valid(self):
        """
        Verifies that a dish is successfully created when valid data is provided
        """
        # pylint: disable=no-member
        Dish.objects.create(name="Pizza", price=10.5)
        self.assertEqual(Dish.objects.count(), 1)

Types of Tests in Django

  • Unit tests: verify small, isolated pieces of logic, e.g., models, utility functions. For example:

    class TestProductModel(TestCase):
      def test_string_representation_returns_name(self):
        product = Product(name="Pizza")
        self.assertEqual(str(product), product.name)
  • Integration tests: ensure multiple components work together correctly. For example:

    class TestSignup(TestCase):
      def test_signup_succeeds_with_valid_data(self):
        response = self.client.post("/signup/", {
          "username": "john",
          "password": "pass123"
        })
        self.assertEqual(User.objects.count(), 1)
  • Functional tests: simulate real user behavior and end-to-end flows. For example:

    def test_login_succeeds_for_valid_credentials(self):
      self.browser.get(self.live_server_url)
      self.browser.find_element(...).click()
  • Regression tests: ensure previously fixed bugs do not reappear. For example:

    def test_registration_fails_with_duplicate_email(self):
      User.objects.create(email="a@test.com")
      with self.assertRaises(IntegrityError):
        User.objects.create(email="a@test.com")

In summary, unit tests check logic, integration tests check how components work together, functional tests check real user behavior, and regression tests ensure old bugs don't comback.

Best Practices for Django Testing

  • Use behavior-driven, feature-grouped naming.
  • Name test methods using the pattern test_[action]_[condition]_[expected].
  • Focus on observable behaviors, not implementation details.
  • Prefix test classes with Test and test methods with test_.
  • Keep tests small, focus, and easy to read.

Little Lemon Project

Introduction

This project implements a small website for a virtual Little Lemon restaurant. It provide six main pages:

  • Home: presents quick‑access cards linking to the menu, reservation form, and opening hours.
  • About: introduces the restaurant, its story, and its owners.
  • Menu: displays the list of dishes offered by the restaurant.
  • Dish: shows detailed information about a specific dish.
  • Reservation: allows customers to reserve a table through an online form.
  • Opening Hours: presents the restaurant's weekly timetable.

To provide a quick overview of the project in action, a short demo video is available. It walks through the main pages, demonstrates the reservation workflow, and highlights the responsive design built with Tailwind CSS, Alpine.js, and Heroicons.

▶ Watch demo video

Project Structure

This website is built with Django, using:

  • Project: littlelemon.
  • Application: restaurant.

The app contains both static and dynamic pages:

  • Static pages rendered using Django's TemplateView: Home, About, and Opening Hours.
  • Dynamic pages backed by two database models:
    • Dish: stores dish information such as name, price, and description, and powers the Menu and individual Dish pages.
    • Reservation: stores reservation data submitted through the reseveration form.

This structure keeps the project lightweight while clearly separating static content from database-driven features.

Forms

The project include a custom form ReservationForm. It inherits from django.forms.ModelForm and include:

  • extend clean_reserved_at method to prevent reservations for a past dates or times.
  • extend clean method to strip leading and trailing whitespace from all CharField and TextField inputs.

Views

The project uses both class-based and function-based views:

  • DishListView inherits from ListView, generates the Menu page.
  • DishDetailView inherits from DetailView, generates individual Dish pages.
  • reserve_table is a function-based view that renders the Reservation page and processes form submissions.

Template Filters

The custom_filters.py module inside the templatetags package defines and registers a custom trim filter, making it available for use in templates.

Admin Configuration

The Django admin interface is customized through:

  • DishAdmin.
  • RersverationAdmin.

Both inherit from django.contrib.admin.ModuleAdmin, providing a clearer and more intuitive interface for managing dishes and reservations.

Frontend Technologies

The templates use:

These tools provide a modern, responsive UI that works smoothly on both desktop and mobile devices.

Installation and Setup

  • Prerequisite: Python 3.10+

  • Clone the repository, then navigate into the littlelemon directory.

    cd littlelemon
  • Create and active a virtual environment.

  • Install dependencies.

    pip install -r requirements.txt
  • Apply database migrations.

    python manage.py migrate
  • Create a superuser (to access the Django admin site).

    python manage.py createsuperuser
  • Run the development server

    python manage.py runserver

    The site will be available at http://localhost:8000.

    The admin panel will be available at http://localhost:8000/admin