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.
- Introduction to Django
- Views
- Models
- Templates
- Little Lemon Project
- 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,colorand new attributes such asplaceholder,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.
- HTML5 introduced semantic tags such as
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.
- 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.
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.
-
A Django project is a Python package containing the database configuration used by various sub-modules (apps) and other Django-specific settings.
-
The
startprojectcommand ofdjango-adminis 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 scriptmanage.py.> django-admin startproject <project_name>
- Project directory is created when we create a Django project. It contains
manage.pyand project package folder. - Project package contains a
settings.pyfile 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.
- Project directory is created when we create a Django project. It contains
-
The
manage.pyscript has the same role as thedjango-adminutility. It can perform everything that thedjango-adminutility does. However, usingmanage.pyis more straightforward, especially if we are required to work on a single project. -
The
startappcommand 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
migratecommand synchronizes the database state with the currently declared models and migrations.> python manage.py migrate -
The
runservercommand starts Django's built-in development server on the local machine.> python manage.py runserver
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.pycontains configuration settings for the Django project, including theINSTALLED_APPSlist where newly created apps must be added.urls.pydefines 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.pyis used by the application servers following the ASGI standard to serve asynchronous web applications.wsgi.pyis the entry point for such WSGI-compatible servers to serve classical web application.
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.
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.
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.
- 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
awaitwhile 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.
- Non-blocking I/O: tasks pause with
- Async shines when we have lots of waiting, not lots of computing.
- 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.
- Concurrency = doing many things seemingly at the same time.
- 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.
- Concurrency: multiple tasks make progress during overlapping time periods.
- 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).
- Concurrency helps with I/O-bound tasks like web requests, database calls, file reads, sleep timers. Examples:
-
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-adminis Django's command line utility for administrative tasks. This utility is present in the scripts folder of the Django environment directory.django-adminutility is executed from inside the terminal.It can also be launched via the call of module
python -m django. -
manage.pyis a script that is the local version ofdjango-adminand is located inside the project folder. It sets the Django settings module environment variable so that it points to our projectsettings.pyfile. -
manage.pyis 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-admincommand with Django settings module or the settings command line option.
manage.pyis more convenient to use thandjango-admin. It runs inside the project folder. When usingdjango-admin, you must set--settingsvariable to the required project'ssettings.pyfile.
-
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
startappcommand option of themanage.pyscript 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.pyA 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.pyfile. -
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.modelsclass. All the models present here are migrated to the database tables.
- 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.
- 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.
- 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.
- 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.pymodule 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.pymodule.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.pyfile.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.
-
-
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
HttpRequestobject, and returns anHttpResponseobject 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.pymodule.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 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.genericmodule. 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"
-
- 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.
- 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.
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 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 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, andDELETE. -
GETmethod:- 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 -
POSTmethod:- 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" } -
PUTmethod:- 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
5with the provided data.PUT /users/5 { "name": "John", "age": 25 } -
PATCHmethod:- 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
ageof the user with id5.PATCH /users/5 { "age": 25 } -
DELETEmethod:- 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.
- 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.).
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 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 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.
- 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:
- 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.
- 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.
- 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.
- secure encrypted tunnel is establish.
- Now, both browser and server share the same secrete session key.
- They use symmetric encryption to exchange data securely.
- encrypted data transfer begins. Every request/response is encrypted: URLs (except domain), cookies, form data, API calls, headers (partially).
- 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.
- client -> server: "Hello!". When we visit a HTTPS site, the browser sends the server a hello message which contains:
- Django handles the request and response with the help of
HttpRequestandHttpResponseclasses in thedjango.httpmodule. - Django obtains the
HttpRequestobject 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
HttpRequestobject as the first argument so that all the request metadata is available to the view for processing.
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.methodreturns the HTTP method that the client used to send request to the server.request.GETandrequest.POSTreturn a dictionary-like object containing GET and POST parameters, respectively.request.COOKIESreturns 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 ofUploadedFileobjects.request.usercontains information about the current user. It's an object ofdjango.contrib.auth.models.Userclass. If the user is unauthenticated, it returnsAnonymousUser.request.has_key()helps check whether theGETorPOSTparameter dictionary has a value for the given key.
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_codereturns the HTTP status code corresponding to the response.contentreturns the byte string of the response.write()creates a file-like object.
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
httporhttps. 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.,
.comaddress can indicate a comercial entity.
- second level domain refers to an organization or the name of a company. e.g.,
- 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.
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.
- A path parameter is a variable part of the URL that is used to identify a specific resource, such as
/customers/5, where5is 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
5as the customer idpkparameter.path("custmers/<int:pk>/", views.customer_detail, name="customer_detail"),
-
The parameter is parsed as
pkparameter and picked by theviews.customer_detail()function. -
The view
customer_detailfunction needs an additional parameterpk, as shown in the following example, because an argument was passed inside theurls.pyfile.def customer_detail(request: HttpRequest, pk: int): pass
-
The parameter names added inside the
pathfunction in theurls.pyfile must match the ones added inside thecustomer_detail()view function associated with it in theviews.pyfile. -
Best pratice: avoid overly verbose parameter names such as
customer_id, prefer the conventionalpk.
-
- 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 anint.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.
-
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.GETproperty. The request object'sGETproperty 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 parameters are data sent in the body of a
POSTrequest, typically from an HTML form, which is not visible in the URL. -
Values can be get via request object's
POSTprogerty, as demonstrated below.def customers(request: HttpRequest): name = request.POST.get("name") age = request.POST.get("age")
- URL dispatcher is Django's mechanism that uses patterns that are defined by URL mapping in
urls.pyto route request to the correct view. - How it works:
- a request comes in, e.g.,
/customers/5/. - Django removes the domain name and leading slashes.
- URL dispatcher looks at the
urlpatternslist inurls.pyfile(s). - It checks each pattern, from top to bottom.
- The first matching pattern triggers the corresponding view.
- Django calls that view and returns the response.
- a request comes in, e.g.,
-
URL mapping is a set of URL patterns that are defined in
urls.pyfile(s). It's a list of instruction or a table of routes. -
Components of URL mapping:
- URL patterns: written in
urls.pyusingpath()orre_path()function. - Views: functions or classes that handle the request.
- Arguments / Parameters: dynamic segments like
<int>or<slug>. - Names: each URL can be given a
namefor 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
- URL patterns: written in
-
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 thedjango.urlsmodule.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"), ]
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/10toexample.com/menu-items/10/.Hence, the pattern
menu-items/10/works with bothexample.com/menu-items/10andexample.com/menu-items/10/, butmenu-items/10doesn't work withexample.com/menu-items/10/. -
NOT include a leading slash.
Django does not expect leading slash, so
/menu-item/10/won't matchexample.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.
-
The application namespace is created by defining the
app_namevariable in the applications'surls.pymodule 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_namedefines 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
includefunction while adding an app'surlpatterns. 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 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.pymodule.path("menu-items/<int:pk>/<str:dish>/", views.menu_item_detail, name="menu_item_detail")
-
Using
reverse()function in theviews.pymodule 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.
-
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.)
- When
-
Custom error pages: we can override Django's default error pages by creating templates in the project
/templatesfolder: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.pymodule 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
HttpResponseor raise an exception. For example:- returns a
HttpResponseNotFound, which is a subclass ofHttpResponsethat specifically indicates a 404 error. It internally sends an error code404. Other predefined subclasses includeHttpResponseBadRequestandHttpResponseForbidden. - raises a
Http404exception, which is a class defined in thedjango.core.exceptionsmodule. Some important exception types are:ObjectDoesNotExist,EmptyResultSet, andFieldDoesNotExist.
- returns a
-
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
supermethod 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
Cclass inherits from bothAandBclasses. Both of them have thehellomethod, Python chooses the one ofA, notB. That decision is based on the MRO. -
The built-in
mromethod 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 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") ]
- use kebab-case (hyphen-separated) for URL paths, snake_case for URL names (the
-
Naming URL paths for CRUD operations: use the pattern
[resources]/[instance]/[action]to keep routes predictable and consistent.For example:
employees/: list viewemployees/<int:pk>/: detail viewemployees/create/: create viewemployees/<int:pk>/update/: update viewemployees/<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.
-
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'smodels.pyfile. 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)
-
pkstands 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 usepkin any ORM operations. For example:User.objects.get(pk=2) User.objects.filter(pk__in=[1, 2, 3])
-
idfield: when declaring a model,- if no field is explicitly defined as the primary key, Django automatically creates an auto‑incrementing
idfield to serve as the primary key. - if a specific field is defined as the primary key, Django does not add the default
idfield.
- if no field is explicitly defined as the primary key, Django automatically creates an auto‑incrementing
-
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()
-
-
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 bymax_lengthparameter. -
TextField: is similar toCharField, 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 mappingIntegerFieldto the databaseINTEGER/INTdata 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'sdatetime.datetimeclass. -
DateField: storesdatetime.datevalue. -
EmailField: is aCharFieldwith an in-builtEmailValidator. -
URLField: is aCharFieldhaving in-built validation for URL. -
FileField: used to save the file uploaded by the user to a designated path specified by theupload_toparameter.
-
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
NULLcolumns 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,
Usertable is accessible via auth service whileUserProfileis accessible via profile service. - Database and performance reasons: some data like
Useris queried constantly while some other likeUserProfileis 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_deleteoption 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
ProtectedErrorif 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
CollegeandPrincipalis set to PROTECT, the deletion will be blocked and aProtectedErrorwill be raised. - if the relationship between
CollegeandPrincipalis set RESTRICT, the deletion will be succeed because Django recognizes that the referenced object will be deleted as part of the same operation.
- if the relationship between
-
Note: when
deletemethods 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.
-
- 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:
makemigrationscreates migration scripts that reflect changes made to models, which are then applied to the database.migrateapplies the migration scripts to the database, creating or modifying tables as defined in the migration files.sqlmigrateshows the SQL query or queries executed when a certain migration script is run.showmigrationsdisplays 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 todb_tableparameter of theMetaclass, 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"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()-
Manageris 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
Managermethod returnsQuerySetobject(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.
-
A
QuerySetis 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
lenfunction.
-
-
How to find the SQL query generated? the
QuerySet'squeryattribute 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.
-
Create a row in the table:
-
create an object of the model class then use the
savemethod to creates a row in the table. For example:m = Menu(name="pho", cuisine="vietnam", price=12) m.save()
-
use the
createmethod of theManager. 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
allmethod of theManager. For example:Menu.objects.all()
-
apply filters to the data fetched from the model by using
filtermethod of theManager.Menu.objects.filter(name__startswith="p")
-
-
Update a row: get the object of that row, assign a new value to the attribute and
savethe 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
deletemethod. For example:m = Menu.objects.get(pk=4) m.delete()
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)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)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)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")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")-
A form is a document wherein the user enters their responses at certain labeled placeholders.
-
Django includes a
Formclass, its attributes, and methods in thedjango.formsmodule. 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
Fieldclass objects. Thedjango.formsmodule has a collection ofFieldtypes. These fields correspond to the HTML elements they enventually render on the user's browser. For example:-
the
forms.CharFieldis translated to HTML's text input type. -
the
forms.ChoiceFieldis 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.pyfile in the app's package folder.
Some of the most frequently used fields are as follow:
-
CharField: translates to inputtype=textHTML form element.forms.CharField(label="Name of Application", max_length=50)
Set the field's
widgetproperty toforms.Textareato create atextarea.forms.CharField(label="Name of Application", widget=forms.Textarea)
-
EmailField: aCharFieldthat can validate if the text entered is a valid email.forms.EmailField(max_length=254)
-
IntegerField: similar to aCharFieldbut customized to accept only integer numbers. We can limit the value entered by settingmin_valueandmax_valueparameters.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 toFloatFieldbut supports fixed numbers of decimal places.forms.DecimalField(max_digits=10, decimal_places=2)
max_digitsanddecimal_placesare mandatory parameters of this field type. -
FileField: presents an inputtype=fileelement on the HTML form.forms.FileField(upload_to="uploads/")
-
ImageField: similar toFileFieldwith 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'sselectelement. Populates the drop-down list with achoicesparameter 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
widgetproperty toforms.RadioSelectto create radio buttons.forms.ChoiceField(choices=PAYMENT_CHOICES, widget=forms.RadioSelect)
-
In Django
FormandModelFormclasses,Metaclass 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
Metaclass 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-
Modelforms,Metais optional and rarely needed. For example:from django import forms class ContactForm(forms.Form): email = forms.EmailField() class Meta: fields = ["email"]
-
Common
Metaoptions include:-
model: specifies which model the form is based on. For example:model = Dish
-
fields: lists the model fields to include. Preferfieldswhenever 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:
- Field-level validation: required fields (
blank=False), type checks (e.g.,DecimalField,CharField, etc.), and built-in validators (e.g.,MinValueValidator,MaxValueValidator, etc.). clean_[field]()methods.clean()for form-wide validation.- Model-level validation:
unique=True,unique_together,constraints, etc.
- Field-level validation: required fields (
-
We can still override or extend validation using
clean_[field]()orclean()methods. -
Metais required forModelForm, optional and usually ignored forForm. -
The
Metashould contain configuration, not business logic.
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>
-
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.Formclass provides theis_validmethod to run validation on each field and returnTrueif 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
Forminstance is validated, we can access the data individual field via itscleaned_dataattribute. 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"]
-
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.comwhile still logged inbank.com. evil.comcontains a hidden HTML form to send a request tobank.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.
- The user logs into
-
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,DELETEwhen using forms or modifying server-side data. - CSRF token not required for
GETrequests and read-only views.
- CSRF token required for
-
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
createsuperusercommand as follows.> python manage.py createsuperuser -
If a user's
is_staffproperty is set toTrue, 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")
-
The
UserAdminclass from thedjango.contrib.auth.adminmodule allows developers to control which fields are editable and to implement additional security measures. -
To customize the User Admin, we first extend the
UserAdminclass in the app'sadmin.pyfile, then unregister the defaultUsermodel 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
Usermodel first, then use theadmin.registerdecorator 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
CustomUserAdminclass prevents users from modifying thelast_loginanddate_joinedfields. It also prevents non-superusers from changing theusername, 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_APPSlist. For example:# settings.py INSTALLED_APPS = [ "app_loaded_first", "app_loaded_second", ]
-
Best practice: create one app responsible for the User 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.pyfile. 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.pyfile. 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
ModelAdminclass, imported fromdjango.contrib.adminmodule, 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)
-
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_staffandis_superuserproperties toTruemakes a user a staff user or a superuser, respectively. -
The permission mechanism is handled by the
django.contrib.authapp. -
When a model is created, Django automatically creates
add,change,delete, andviewpermissions. These permissions follow the naming pattern[app].[action_model]pattern.app: is the application name.action: isadd,change,delete, orview.model: is the model name in lowercase.
For instance,
my_app.add_mymodelrepresents the permission required to add aMyModelinstance in themy_appapplication. -
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.
-
Django app receives user information through the
requestcontext. -
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_anonymousfunction of therequest.userobject. 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_requireddecorator, imported fromdjango.contrib.auth.decoratorsmodule. 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_testdecorator, imported fromdjango.contrib.auth.decoratorsmodule. 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_requireddecorator, imported fromdjango.contrib.auth.decoratorswith 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
PermissionRequiredMixinclass, imported fromdjango.contrib.auth.mixinswith 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
userandpermsvariables 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.pyto 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.
-
- 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.
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.pyby updating theDATABASESsetting with the correctENGINE,NAME,USER,PASSWORD,HOST,PORT, and any requiredOPTIONS. 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 dbshellIf the command opens a DB shell (
psql,mysql, etc.), the config works.
-
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-decoupleis 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.envfiles in a clean and safe way. It:- reads environment variables first,
- falls back to a
.envfile (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")
-
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
renderfunction, imported fromdjango.shortcutsin view functions. -
The
renderfunction takes three parameters:- a
requestobject: 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)
- a
- 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 %}
-
-
Django uses template loaders to locate templates based on the configuration defined in the
TEMPLATESsection in the project'ssettings.pyfile. -
TEMPLATESis 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. Usingdjango.template.backends.django.DjangoTemplatesmeans 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
templatesfolder. -
APP_DIRS: indicates whether Django should search for templates inside installed apps.True: Django looks for templates in atemplatesfolder within each installed app. The folder nametemplatesis 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 (ifAPP_DIRSis set toTrue). - 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.
- It first searches the directories listed in
-
Best practice:
-
Use the directories listed in
DIRSfor shared or base templates. -
Use each installed apps's
templatesdirectory 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 therequestobject to templates.django.contrib.auth.context_processors.auth: adds authentication-related variablesuser,perms.django.contrib.messages.context_processors.messages: adds Django messages framework support. It is used for success messages, error notifications, alerts.
-
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 %} - use
-
How it works?
- Django loads the child template.
- It encounters the
{% extends %}tag and then loads the base template. - It replace the base template's
{% block %}sections with the content provided by the child template. - The result is a complete HTML page that combines the base layout with the child template's content.
-
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.
- only one
-
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
-
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.staticfilesapp, 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_URLsetting insettings.pydefines 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_URLdoes not tell Django where static files are stored. It only defines how they are acessed via a URL. -
The
STATICFILES_DIRSsetting insettings.pytells 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:
- app-level
static/directories (inside each installed app). - directories listed in
STATICFILES_DIR.
STATICFILES_DIRis not set by default because Django prioritizes app-level static files. We should defineSTATICFILES_DIRwhen 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.
- app-level
-
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
-
Django's testing system is built on Python's
unittestframework, with additional Django-specific extensions. -
Tests follow a class-based structure: each test class inherits from
django.test.TestCase, and individual test methods begin withtest_. -
The
testmanagement command is used to run the test suite.python manage.py testWhen 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.
-
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 """
-
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)
-
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.
- 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
Testand test methods withtest_. - Keep tests small, focus, and easy to read.
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.
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.
The project include a custom form ReservationForm. It inherits from django.forms.ModelForm and include:
- extend
clean_reserved_atmethod to prevent reservations for a past dates or times. - extend
cleanmethod to strip leading and trailing whitespace from allCharFieldandTextFieldinputs.
The project uses both class-based and function-based views:
DishListViewinherits fromListView, generates the Menu page.DishDetailViewinherits fromDetailView, generates individual Dish pages.reserve_tableis a function-based view that renders the Reservation page and processes form submissions.
The custom_filters.py module inside the templatetags package defines and registers a custom trim filter, making it available for use in templates.
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.
The templates use:
These tools provide a modern, responsive UI that works smoothly on both desktop and mobile devices.
-
Prerequisite: Python 3.10+
-
Clone the repository, then navigate into the
littlelemondirectory.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