A small Django eCommerce site for a coffee shop, styled with Bootstrap 5 and a custom stylesheet layered on top. Visitors browse a catalogue of coffee gear and beans, fill a shopping cart without needing an account, and register or log in when they check out. Orders reduce stock, an invoice is emailed (to the terminal in development), and a "Contact us" form in the footer sends enquiries to the shop owner.
- Python 3 + Django 6.0
- Bootstrap 5.3.8 and Font Awesome, loaded from a CDN in
base.html - Custom CSS in
shop/static/style.css, linked after Bootstrap so its rules win - SQLite (
db.sqlite3), Django's built-in file-based database - no separate database server, so setup is a singlemigratestep - Server-rendered Django templates (no separate front end)
Run these from the coffee_shop/ folder (the one containing manage.py):
# 1. Create and activate a virtual environment
python -m venv venv
venv\Scripts\activate # Windows (PowerShell / cmd)
# source venv/bin/activate # macOS / Linux
# 2. Install the dependencies
pip install -r ../requirements.txt
# 3. Create the database and load the starting stock
python manage.py migrate
# 4. Start the development server
python manage.py runserverThe site is then available at http://127.0.0.1:8000/.
requirements.txt lives in the folder above manage.py, which is why step 2
uses ../requirements.txt. It pins Django 6.0.7 and the three packages Django installs alongside
itself (asgiref, sqlparse, tzdata), so everyone gets the same versions:
asgiref==3.12.1
Django==6.0.7
sqlparse==0.5.5
tzdata==2026.3
The twelve starting products are loaded by a data migration (shop/migrations/0004_seed_products.py),
so the shop has stock as soon as migrate has run. Running it again will not create duplicates.
No secret key or email address is stored in the repository. Everything configurable is read from the environment, with development-friendly defaults so the steps above work with nothing set:
| Variable | Default | Purpose |
|---|---|---|
DJANGO_SECRET_KEY |
a new random key each start-up | Signs sessions and password-reset tokens. Must be set to a fixed value in production |
DJANGO_DEBUG |
True |
Set to False in production |
DJANGO_ALLOWED_HOSTS |
localhost,127.0.0.1 |
Comma-separated hostnames the site will answer to |
CONTACT_EMAIL |
shop-owner@example.com |
Where the "Contact us" form sends enquiries |
Because the development fallback generates a fresh secret key on every start, restarting the
server signs out anyone who was logged in. Set DJANGO_SECRET_KEY if you want sessions to
survive a restart.
The suite covers the areas where a mistake would be expensive: the cart's stock ceiling, the
all-or-nothing stock check at checkout, the price captured on each order line, and the rule that
a buyer can only open their own orders. Registration, login (including the check that a
hand-crafted next cannot bounce a user off-site), the contact form and the password-reset flow
are covered too.
python manage.py test shopRan 53 tests in 15.030s
OK
| Page | URL | Notes |
|---|---|---|
| Home / product grid | / |
Open to everyone. "Buy" adds one item to the cart |
| Cart | /cart/ |
Remove one, remove all, or continue to checkout |
| Register | /register/buyer/ |
Creates the account and logs you straight in |
| Login | /login/ |
Session expires after one day |
| Forgot password | /password-reset/ |
Emails a one-time reset link (see below) |
| Checkout | /checkout/ |
Requires an account - signed-out shoppers are sent to login and come back |
| Confirmation | /confirmation/<order id>/ |
Shown after a successful order |
| Order detail | /orders/<order id>/ |
A buyer can only see their own orders |
| Django admin | /admin/ |
Manage products, orders and buyers |
The cart is stored in the session, so anyone can fill one without signing up, and it survives logging in. Quantities are capped at the stock actually on hand - asking for more than is available shows a warning rather than over-selling. Out-of-stock products show a disabled "Sold out" button.
Checkout requires a logged-in user. The whole cart is checked for stock before anything is
written, so one sold-out item cannot leave a half-filled order behind with earlier products'
stock already deducted. The order and its line items are then created inside a single database
transaction, stock is reduced, and the price of each item is stored on the order line
(price_at_checkout), so later price changes don't rewrite order history.
The Django admin is used to manage products, orders and buyers. Create a superuser from the command line:
python manage.py createsuperuserThen sign in at /admin/.
Email is printed to the terminal running runserver, not delivered to a real inbox:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"No mail server or credentials are needed. To reset a password:
- Go to
/password-reset/and enter the email address on the account. - Switch to the terminal running the server and find the "Password Reset" message.
- Copy the link it contains (
http://127.0.0.1:8000/reset/<uid>/<token>/), open it in the browser, and set a new password.
The same page is shown whether or not the email matched an account, so the form can't be used to find out which addresses are registered.
The "Contact us" form in the home page footer sends to CONTACT_EMAIL. That defaults to the
placeholder shop-owner@example.com and can be overridden with an environment variable, so no
real address is committed to the repository.
Bootstrap is loaded from the CDN in shop/templates/base.html, with style.css linked after it
so the custom rules take precedence where they overlap.
| Bootstrap feature | Where it is used |
|---|---|
Grid (row, row-cols-*, col-md-*) |
Product cards, three to a row on medium screens and up, wrapping automatically for any number of products |
| Responsive navbar with collapse toggler | base.html - links collapse to a hamburger menu on small screens |
Horizontal form (col-form-label, form-control) |
"Contact us" form in the home page footer |
| Toasts | Pop-up confirmations for "added to your cart", removals and contact-form results |
Utility classes (d-flex, ms-auto, mt-4, fw-bold) |
Spacing and alignment throughout |
img-fluid |
Product photos, overridden in style.css to a fixed height with object-fit: cover so the grid stays even |
The custom stylesheet defines the coffee theme (browns, cream and gold) as CSS variables in
:root and reuses them throughout, and adds the zoom-and-glow hover effect on product images.
| Model | Purpose |
|---|---|
Product |
An item for sale: title, description, price, stock quantity and the filename of its photo. in_stock is True while quantity is above zero |
Buyer |
A customer, linked one-to-one to Django's built-in User, with a saved delivery address |
Order |
A buyer's order: shipping address, status and date. total adds up its line items |
OrderItem |
One product line on an order, holding the quantity and the price at the time of purchase |
coffee_shop/
├── README.md # this file
├── .gitignore # keeps the database, venv and caches out of git
├── requirements.txt # pinned Python dependencies
├── website.html # a standalone Bootstrap grid, kept as a reference
└── coffee_shop/
├── manage.py # Django command-line entry point
├── db.sqlite3 # SQLite database - created locally by `migrate`
├── coffee_shop/ # project configuration
│ ├── settings.py # settings (database, static files, email backend)
│ ├── urls.py # root URL configuration
│ └── wsgi.py / asgi.py # server entry points
└── shop/ # the main application
├── models.py # Product, Buyer, Order, OrderItem
├── views.py # all page logic, auth and password reset
├── urls.py # URL routes (namespaced under "shop")
├── forms.py # buyer registration form
├── admin.py # Django admin registrations
├── tests.py # the test suite
├── migrations/ # schema migrations + the product seed data
├── templates/
│ ├── base.html # shared layout: Bootstrap, navbar, toasts
│ └── shop/ # home, cart, checkout, confirmation, auth pages
└── static/
├── style.css # custom CSS layered over Bootstrap
└── assets/ # product photographs
