Building a CRUD (Create, Read, Update, Delete) application is one of the first and most important steps in learning Django. CRUD forms the backbone of many web applications, providing essential functionality for managing data. In this guide, we’ll create a simple supplier management system for a fictional retail company, making the learning process both practical and engaging. Designing an app for a business scenario not only improves technical skills but also helps understand real-world applications of Django in data-driven systems. If you want to understand the basic of Django, you may refer to my previous post – Understanding the Basic of Django.
Takeaways from This Post
- Have a working CRUD application built with Django.
- Understand how to create, read, update, and delete supplier records.
- Gain hands-on experience with Django models, forms, views, and templates.
- Learn how to set up URL routing and work with database migrations.
What is CRUD and Why is it Important?
CRUD stands for Create, Read, Update, and Delete—the four basic operations used to manage data in any application. These operations are fundamental because they mirror the basic tasks users perform in most systems, whether it’s managing a blog, an inventory system, or user profiles.
Mastering CRUD in Django helps you understand:
- How to structure a Django project and app.
- The workflow for creating models, views, and templates.
- How Django handles form data and interacts with the database.
With CRUD as your foundation, you can easily expand into more complex features like authentication, API development, and advanced database relationships.
Requirements
Before you start, ensure you have Python installed on your machine. For this tutorial, you will need:
- Minimum Python Version: 3.8
- Minimum Django Version: 5.1.3
This guide has been tested using Python 3.10.11 and Django 5.1.3
Fully tested source code can be cloned from GitHub
https://github.com/henryphchan/henryc48.sg-host.com/tree/main/Python/Django/retail_project
Step 1: Create Your Django Project and Supplier App
1.1 Open your terminal and create a new project:
django-admin startproject retail_project
cd retail_project1.2 Inside your project, create a new app:
python manage.py startapp supplier_app1.3 Register the app in settings.py:
INSTALLED_APPS = [
...
'supplier_app', # Add this line
]Step 2: Define the Supplier Model
Models are the foundation of your Django application, defining the structure of your database tables. Each model represents a single table, with its fields corresponding to table columns. In Django, models are defined using Python classes, which Django’s ORM (Object-Relational Mapping) automatically translates into SQL database schemas. By using models, you can efficiently manage, query, and manipulate database records without writing raw SQL. Models also support relationships, constraints, and custom behaviors, making them a powerful tool for structuring your application’s data.
2.1 Open supplier_app/models.py and add:
from django.db import models
class Supplier(models.Model):
ACTIVE_STATUS_CHOICES = [
('Active', 'Active'),
('Inactive', 'Inactive')
]
SupplierID = models.AutoField(primary_key=True) # Automatically generated unique ID for each supplier.
Name = models.CharField(max_length=100) # Stores supplier names as strings (up to 100 characters).
ContactPerson = models.CharField(max_length=100) # Stores the supplier's contact person.
ContactEmail = models.EmailField(max_length=100, blank=True, null=True) # Stores an optional email with email validation.
PhoneNumber = models.CharField(max_length=20, blank=True, null=True) # Stores an optional phone number.
Address = models.TextField(blank=True, null=True) # Stores an optional address.
ActiveStatus = models.CharField(max_length=8, choices=ACTIVE_STATUS_CHOICES) # Dropdown with "Active" or "Inactive" options.
ContractStartDate = models.DateField(blank=True, null=True) # Stores an optional contract start date.
ContractEndDate = models.DateField(blank=True, null=True) # Stores an optional contract end date.
"""
This method ensures that when a Supplier object is printed or displayed, its Name will be shown instead of the default object representation.
"""
def __str__(self):
return self.NameThe table name for a model is automatically generated based on the app name and the model name. By default, Django converts the model class name to lowercase and prefixes it with the app name. Since this model is inside supplier_app, Django will generate a table name supplier_app_supplier.
2.2 If you want to explicitly define a custom table name, you can add the Meta class inside the model:
class Supplier(models.Model):
class Meta:
db_table = "custom_supplier_table"2.3 Once the model is defined, we need to apply it to the database so Django can create the corresponding table. This is done using migrations, which track changes in database structure over time. Running makemigrations generates a migration file that records the model changes, and migrate applies those changes to the actual database.
python manage.py makemigrations
python manage.py migrateStep 3: Create a Supplier Form
3.1 The forms.py file in your Django project defines a form class which will make it easy to manage input:
- Automatically generates form fields from the
Suppliermodel. - Validates user input before saving it to the database.Provides a clean and structured way to handle form submissions in Django.
from django import forms
from .models import Supplier
class SupplierForm(forms.ModelForm):
class Meta:
model = Supplier
fields = ['Name', 'ContactPerson', 'ContactEmail', 'PhoneNumber', 'Address', 'ActiveStatus', 'ContractStartDate', 'ContractEndDate']
widgets = {
'ActiveStatus': forms.Select(choices=Supplier.ACTIVE_STATUS_CHOICES),
}
def clean_ContactEmail(self):
email = self.cleaned_data.get('ContactEmail')
if email and not forms.EmailField().clean(email):
raise forms.ValidationError("Please enter a valid email address.")
return email
def clean(self):
cleaned_data = super().clean()
start_date = cleaned_data.get('ContractStartDate')
end_date = cleaned_data.get('ContractEndDate')
if start_date and end_date and start_date > end_date:
self.add_error('ContractStartDate', "Contract start date cannot be greater than contract end date.")
self.add_error('ContractEndDate', "Contract end date cannot be earlier than contract start date.")
return cleaned_datamodel = Suppliertells Django that this form is based on theSuppliermodel.fieldsspecifies which model fields should be included in the form.widgetsallows customization of how fields are displayed.ActiveStatusis rendered as a dropdown (Select) with predefined choices.
The clean function in Django forms is used to validate multiple fields together and apply custom validation logic before saving form data. Calling super.clean() ensures that Django’s default validation is applied before custom logic is added. It allows validation across multiple fields, so we added custom logic to ensure start_date is not later than end_date. If validation fails, it adds errors to the respective fields, preventing form submission.
If we want to customizing validation logic of a specific field, we can add a clean_FieldName Method – clean_ContactEmail. This function ensures that ContactEmail contains a valid email format and raise a custom error message.
Step 4: Create Views for CRUD Operations
4.1 In supplier_app/views.py, add the following code snippets:
from django.shortcuts import render, redirect, get_object_or_404
from .models import Supplier
from .forms import SupplierForm
from django.contrib import messages
render: Renders HTML templates.redirect: Redirects to another view after form submission.get_object_or_404: Retrieves an object from the database or returns a 404 error if not found.Supplier: The database model representing a supplier.SupplierForm: The Django form handling supplier input.messages: Provides user feedback messages.
# List all suppliers
def supplier_list(request):
suppliers = Supplier.objects.all()
return render(request, 'supplier_app/supplier_list.html', {'suppliers': suppliers})- Retrieves all supplier records from the database.
- Passes them to the
supplier_list.htmltemplate for rendering.
# Create a new supplier
def supplier_create(request):
if request.method == 'POST':
form = SupplierForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, "Supplier has been added successfully.")
return redirect('supplier_list')
else:
form = SupplierForm()
return render(request, 'supplier_app/supplier_form.html', {'form': form})
- If the request is
POST, the form is validated and saved to the database. - A success message is displayed using
messages.success(). - If the request is
GET, an empty form is displayed. form.is_valid()is a method that checks whether the data submitted in a form meets the validation rules defined in the form class. It executes any custom validation logic (e.g.,clean_FieldName()andclean()methods).
# Update an existing supplier
def supplier_update(request, pk):
supplier = get_object_or_404(Supplier, pk=pk)
if request.method == 'POST':
form = SupplierForm(request.POST, instance=supplier)
if form.is_valid():
form.save()
messages.success(request, "Supplier has been updated successfully.")
return redirect('supplier_list')
else:
form = SupplierForm(instance=supplier)
return render(request, 'supplier_app/supplier_form.html', {'form': form, 'supplier': supplier})
- Retrieves the supplier by
pk(primary key). - If the request is
POST, updates the supplier’s details. - Otherwise, displays the form pre-filled with existing data.
# Delete an existing supplier
def supplier_delete(request, pk):
supplier = get_object_or_404(Supplier, pk=pk)
if request.method == 'POST':
supplier.delete()
messages.success(request, "Supplier has been deleted successfully.")
return redirect('supplier_list')
return render(request, 'supplier_app/supplier_confirm_delete.html', {'supplier': supplier})- Retrieves the supplier by
pk. - If confirmed via
POST, deletes the record. - Otherwise, renders a confirmation page.
Step 5: Set Up URLs
5.1 Add URLs for the app in supplier_app/urls.py:
from django.urls import path
from . import views
urlpatterns = [
path('', views.supplier_list, name='supplier_list'),
path('new/', views.supplier_create, name='supplier_create'),
path('edit/<int:pk>/', views.supplier_update, name='supplier_update'),
path('delete/<int:pk>/', views.supplier_delete, name='supplier_delete'),
]path('', views.supplier_list, name='supplier_list'):- This maps the root URL (
/supplier/) tosupplier_list(), displaying a list of suppliers. - The
name='supplier_list'allows template referencing using{% url 'supplier_list' %}.
- This maps the root URL (
path('new/', views.supplier_create, name='supplier_create'):- This maps
/supplier/new/tosupplier_create(), which displays a form to add a new supplier.
- This maps
path('edit/<int:pk>/', views.supplier_update, name='supplier_update'):- This maps
/supplier/edit/ID/tosupplier_update(), whereIDis a dynamic integer parameter (pk). - It allows editing an existing supplier by passing its primary key (
pk).
- This maps
path('delete/<int:pk>/', views.supplier_delete, name='supplier_delete'):- This maps
/supplier/delete/ID/tosupplier_delete(), confirming and deleting a supplier.
- This maps
5.2 Update the urls.py in your project folder (retail_project):
from django.contrib import admin
from django.urls import path, include
from django.views.generic import TemplateView
urlpatterns = [
path('', TemplateView.as_view(template_name='index.html'), name='index'),
path('admin/', admin.site.urls),
path('supplier/', include('supplier_app.urls')),
]Path Definition:
'supplier/'defines a URL prefix for all supplier-related routes.- This means that any URL starting with
/supplier/will be handled by thesupplier_app.
Including App-specific URLs:
- Instead of listing all supplier-related URLs here, Django allows modularity by using
include('supplier_app.urls'). - This redirects Django to look inside
supplier_app/urls.pyfor more specific routes.
Step 6: Create Templates
We are using Flowbite as a CSS framework via CDN. Flowbite provides pre-designed components like forms, buttons, and toast messages, helping us maintain a modern and consistent UI. By using the CDN version, we avoid installing extra dependencies, allowing us to focus on writing Django code without worrying about front-end styling.
6.1 Create the Base Template in retail_project/templates/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/flowbite@2.5.2/dist/flowbite.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<title>Retail Project</title>
</head>
<body class="mx-2">
<nav class="bg-white dark:bg-gray-800 antialiased">
<div class="max-w-screen-xl px-4 mx-auto 2xl:px-0 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-8">
<div class="shrink-0">
<a href="{% url 'index' %}" class="flex items-center justify-between mr-4">
<img
src="https://henrychan.tech/wp-content/uploads/2024/11/cropped-avataaars-150x150.png"
class="mr-3 h-8"
alt="SmallApp.Dev Logo"
/>
<span class="self-center text-2xl whitespace-nowrap text-gray-900 dark:text-white">Retail</span>
</a>
</div>
<ul class="hidden lg:flex items-center justify-start gap-6 md:gap-8 py-3 sm:justify-center">
<li>
<a href="{% url 'index' %}" title="" class="flex text-sm font-medium text-gray-900 hover:text-primary-700 dark:text-white dark:hover:text-primary-500">
Home
</a>
</li>
<li class="shrink-0">
<a href="{% url 'supplier_list' %}" title="" class="flex text-sm font-medium text-gray-900 hover:text-primary-700 dark:text-white dark:hover:text-primary-500">
Supplier
</a>
</li>
<li class="shrink-0">
<a href="{% url 'index' %}" title="" class="flex text-sm font-medium text-gray-900 hover:text-primary-700 dark:text-white dark:hover:text-primary-500">
Product
</a>
</li>
</ul>
</div>
<div class="flex items-center lg:space-x-2">
<button type="button" data-collapse-toggle="ecommerce-navbar-menu-1" aria-controls="ecommerce-navbar-menu-1" aria-expanded="false" class="inline-flex lg:hidden items-center justify-center hover:bg-gray-100 rounded-md dark:hover:bg-gray-700 p-2 text-gray-900 dark:text-white">
<span class="sr-only">
Open Menu
</span>
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M5 7h14M5 12h14M5 17h14"/>
</svg>
</button>
</div>
</div>
<div id="ecommerce-navbar-menu-1" class="bg-gray-50 dark:bg-gray-700 dark:border-gray-600 border border-gray-200 rounded-lg py-3 hidden px-4 mt-4">
<ul class="text-gray-900 dark:text-white text-sm font-medium dark:text-white space-y-3">
<li>
<a href="{% url 'index' %}" class="hover:text-primary-700 dark:hover:text-primary-500">Home</a>
</li>
<li>
<a href="{% url 'supplier_list' %}" class="hover:text-primary-700 dark:hover:text-primary-500">Supplier</a>
</li>
<li>
<a href="{% url 'index' %}" class="hover:text-primary-700 dark:hover:text-primary-500">Product</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mx-auto">
{% block content %}{% endblock %}
</div>
<footer class="p-4 bg-white md:p-8 lg:p-10 dark:bg-gray-800">
<div class="mx-auto max-w-screen-xl text-center">
<a href="https://henrychan.tech" class="flex justify-center items-center text-2xl font-semibold text-gray-900 dark:text-white">
<img
src="https://henrychan.tech/wp-content/uploads/2024/11/cropped-avataaars-150x150.png"
class="mr-2 h-8"
alt="SmallApp.Dev Logo"
/>
Henry's Dev Journey
</a>
<p class="my-6 text-gray-500 dark:text-gray-400">A data engineer’s diary - Overcoming daily challenges, sharing solutions, and exploring the evolving data landscape.</p>
<span class="text-sm text-gray-500 sm:text-center dark:text-gray-400">Created by <a href="https://henrychan.tech" class="hover:underline">Henry's Dev Journey</a>.</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/flowbite@2.5.2/dist/flowbite.min.js"></script>
</body>
</html>- The base template (
base.html) defines the overall layout of the website, including the navigation bar, footer, and general styling. - The
{% block content %}{% endblock %}section is a placeholder where different pages (likeindex.htmlorsupplier_list.html) can inject their unique content.
6.2 Create the index page in retail_project/templates/index.html
{% extends 'base.html' %}
{% block content %}
<section class="bg-white dark:bg-gray-900">
<div class="py-8 px-4 mx-auto max-w-screen-xl sm:py-16 lg:px-6">
<div class="max-w-screen-md">
<h2 class="mb-4 text-4xl tracking-tight font-extrabold text-gray-900 dark:text-white">Welcome to the Retail Project</h2>
<p class="mb-8 font-light text-gray-500 sm:text-xl dark:text-gray-400">This is the landing page for the retail project.</p>
</div>
</div>
</section>
{% endblock %}index.htmlis one of the child templates ofbase.html. The{% extends 'base.html' %}tells Django thatindex.htmlinherits frombase.html.- The
{% block content %}section replaces the placeholder inbase.htmlwith this specific content.
This approach follows the DRY (Don’t Repeat Yourself) principle, making it easier to maintain a consistent design across multiple pages while allowing flexibility for different content on each page. It also enhances modularity, ensuring that updates to the base template automatically reflect across all child templates, reducing redundant code and improving maintainability.
6.3 Create Supplier List page in supplier_app/templates/supplier_app/supplier_list.html
{% extends 'base.html' %}
{% block content %}
<div class="relative overflow-x-auto">
<div class="max-w-screen-md">
<h2 class="mb-4 text-4xl tracking-tight font-extrabold text-gray-900 dark:text-white">Supplier List</h2>
<a href="{% url 'supplier_create' %}">
<button type="button" class="mt-4 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Add Supplier</button>
</a>
</div>
<table class="w-full mt-4 text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">
Supplier Name
</th>
<th scope="col" class="px-6 py-3">
Contact Name
</th>
<th scope="col" class="px-6 py-3">
Status
</th>
<th scope="col" class="px-6 py-3">
Action
</th>
</tr>
</thead>
<tbody>
{% for supplier in suppliers %}
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ supplier.Name }}
</th>
<td class="px-6 py-4">
{{ supplier.ContactPerson }}
</td>
<td class="px-6 py-4">
{{ supplier.ActiveStatus }}
</td>
<td class="px-6 py-4">
<a href="{% url 'supplier_update' supplier.pk %}">Edit</a> - <a href="{% url 'supplier_delete' supplier.pk %}">Delete</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if messages %}
{% for message in messages %}
<div id="toast-message" class="fixed bottom-4 right-4 p-4 mb-4 flex items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert">
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
</svg>
<span class="sr-only">Check icon</span>
</div>
<div class="ms-3 text-sm font-normal">{{ message }}</div>
<button type="button" class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#toast-message" aria-label="Close">
<span class="sr-only">Close</span>
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
</svg>
</button>
</div>
{% endfor %}
<script>
document.addEventListener("DOMContentLoaded", function () {
setTimeout(function () {
const toastMessage = document.getElementById("toast-message");
if (toastMessage) {
// Add a class to trigger the transition/animation
toastMessage.classList.add("opacity-0", "translate-y-4", "transition", "duration-500");
// Wait for the animation to finish before removing the element
toastMessage.addEventListener("transitionend", function () {
toastMessage.remove();
});
}
}, 3000); // 3000 milliseconds = 3 seconds
});
</script>
{% endif %}
{% endblock %}- This list provides an organized list of suppliers.
- In Django views, we pass data to templates using a context dictionary. In this case, the view function (
supplier_list) retrieves all supplier records and sends them to the template. - The
{% for supplier in suppliers %}loop iterates through the list of suppliers and populates the table dynamically. - It fetches all suppliers in
views.pyusingSupplier.objects.all(). {{ supplier.Name }}retrieves theNamefield from the currentSupplierobject in the loop.
6.4 Create Supplier Form page in supplier_app/templates/supplier_app/supplier_form.html
{% extends 'base.html' %}
{% block content %}
<section class="bg-white dark:bg-gray-900">
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-16">
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-white">{% if supplier %}{{ supplier.Name }}{% else %}Add a new Supplier{% endif %}</h2>
<form method="post">
{% csrf_token %}
<div class="grid gap-4 sm:grid-cols-2 sm:gap-6">
<div class="w-full">
<label for="id_Name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name</label>
<input type="text" name="Name" id="id_Name" value="{% if supplier %}{{ supplier.Name }}{% endif %}" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
{% if form.Name.errors %}
<div class="text-red-500 text-sm">{{ form.Name.errors }}</div>
{% endif %}
</div>
<div class="w-full">
<label for="id_ContactPerson" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Contact Person</label>
<input type="text" name="ContactPerson" id="id_ContactPerson" value="{% if supplier %}{{ supplier.ContactPerson }}{% endif %}" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
{% if form.ContactPerson.errors %}
<div class="text-red-500 text-sm">{{ form.ContactPerson.errors }}</div>
{% endif %}
</div>
<div class="w-full">
<label for="id_ContactEmail" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Contact Email</label>
<input type="email" name="ContactEmail" id="id_ContactEmail" value="{% if supplier and supplier.ContactEmail %}{{ supplier.ContactEmail }}{% else %}{{ '' }}{% endif %}" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
{% if form.ContactEmail.errors %}
<div class="text-red-500 text-sm">{{ form.ContactEmail.errors }}</div>
{% endif %}
</div>
<div class="w-full">
<label for="id_PhoneNumber" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Phone Number</label>
<input type="text" name="PhoneNumber" id="id_PhoneNumber" value="{% if supplier and supplier.PhoneNumber %}{{ supplier.PhoneNumber }}{% else %}{{ '' }}{% endif %}" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
{% if form.PhoneNumber.errors %}
<div class="text-red-500 text-sm">{{ form.PhoneNumber.errors }}</div>
{% endif %}
</div>
<div class="w-full">
<label for="id_Address" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Address</label>
<textarea name="Address" id="id_Address" rows="3" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">{% if supplier %}{{ supplier.Address }}{% endif %}</textarea>
{% if form.Address.errors %}
<div class="text-red-500 text-sm">{{ form.Address.errors }}</div>
{% endif %}
</div>
<div class="w-full">
<label for="id_ActiveStatus" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Active Status</label>
<select name="ActiveStatus" id="id_ActiveStatus" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
<option value="Active" {% if supplier and supplier.ActiveStatus == 'Active' %}selected{% endif %}>Active</option>
<option value="Inactive" {% if supplier and supplier.ActiveStatus == 'Inactive' %}selected{% endif %}>Inactive</option>
</select>
{% if form.ActiveStatus.errors %}
<div class="text-red-500 text-sm">{{ form.ActiveStatus.errors }}</div>
{% endif %}
</div>
<div class="w-full">
<label for="id_ContractStartDate" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Contract Start Date</label>
<input type="date" name="ContractStartDate" id="id_ContractStartDate" value="{% if supplier %}{{ supplier.ContractStartDate|date:'Y-m-d' }}{% endif %}" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
{% if form.ContractStartDate.errors %}
<div class="text-red-500 text-sm">{{ form.ContractStartDate.errors }}</div>
{% endif %}
</div>
<div class="w-full">
<label for="id_ContractEndDate" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Contract End Date</label>
<input type="date" name="ContractEndDate" id="id_ContractEndDate" value="{% if supplier %}{{ supplier.ContractEndDate|date:'Y-m-d' }}{% endif %}" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
{% if form.ContractEndDate.errors %}
<div class="text-red-500 text-sm">{{ form.ContractEndDate.errors }}</div>
{% endif %}
</div>
</div>
<button type="submit" class="inline-flex items-center mt-4 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
{% if supplier %}Update Supplier{% else %}Add Supplier{% endif %}
</button>
<a href="{% url 'supplier_list' %}" class="inline-flex items-center mt-4 text-gray-700 bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-600 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">Cancel</a>
</form>
</div>
</section>
{% endblock %}- The
supplier_form.htmltemplate is responsible for handling supplier creation and editing. This form includes CSRF protection and error handling, which are essential for security. Django automatically enables CSRF protection in forms that modify data (POST requests).The{% csrf_token %}template tag inserts a hidden CSRF token in the form.When the form is submitted, Django verifies this token to ensure the request comes from a trusted source. - Django provides built-in form validation, which prevents users from submitting incorrect or incomplete data. Errors are captured and displayed next to each field. If
form.is_valid()inviews.pyfails, error messages are stored and passed to the template.These errors are displayed using{% if form.FieldName.errors %}in the template.
6.5 Create Supplier Delete page in supplier_app/templates/supplier_app/supplier_confirm_delete.html
{% extends 'base.html' %}
{% block content %}
<section class="bg-white dark:bg-gray-900">
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-16">
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-white">Delete Supplier</h2>
<p class="mb-4 text-sm text-gray-900 dark:text-white">Are you sure you want to delete <strong>{{ supplier.Name }}</strong>?</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="inline-flex items-center mt-4 text-white bg-red-600 hover:bg-red-700 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-500 dark:hover:bg-red-600 focus:outline-none dark:focus:ring-red-800">Yes, delete</button>
<a href="{% url 'supplier_list' %}" class="inline-flex items-center mt-4 text-gray-700 bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-600 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">Cancel</a>
</form>
</div>
</section>
{% endblock %}- The Supplier Delete Page is a confirmation dialog that ensures users do not accidentally delete supplier records. It follows a structured approach with CSRF protection, a confirmation message, and action buttons.
Step 7: Test your app
7.1 Start the development server:
python manage.py runserverVisit http://127.0.0.1:8000/supplier/ to manage suppliers.
Your folder structure will looks like this:
retail_project/
├── db.sqlite3
├── manage.py
├── retail_project/
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── supplier_app/
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── migrations/
│ │ ├── 0001_initial.py
│ │ ├── __init__.py
│ ├── models.py
│ ├── templates/
│ │ └── supplier_app/
│ │ ├── supplier_confirm_delete.html
│ │ ├── supplier_form.html
│ │ └── supplier_list.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
└── templates/
├── base.html
└── index.html
Congratulations! You’ve successfully built a Django CRUD application. Mastering CRUD operations is an essential step in Django development, and you now have a solid foundation in working with models, forms, views, and templates.
But this is just the beginning! The next step is to expand your knowledge by learning how to create more database tables and establish relationships between them. This includes using foreign keys to link related data, setting up one-to-many and many-to-many relationships, and understanding how Django’s ORM (Object-Relational Mapping) makes database management easier.
By adding related tables, you can build more complex applications such as product information systems, inventory systems, customer management tools, or multi-user platforms. Keep experimenting, and soon you’ll be able to create robust and scalable Django applications!
