Python Django PostgreSQL cat adoption
A cat adoption directory lets visitors search by name or temperament, browse shelter rooms, and compare each cat's status and personality. Tokay runs the Django project as one Web Service with PostgreSQL.
Last updated
This is a regular Django project with templates, static styles, models, migrations, and optional admin access. Tokay handles those familiar pieces together, so you do not need a separate asset service or database process.
- Runtime
- Python
- Framework
- Django
- Service types
- Web Service
- Resources
- PostgreSQL
- Required secrets
SECRET_KEY
What it does
The directory introduces every cat with an age, temperament, room, adoption status, and a short note from the shelter.
Search for a name or a quality such as calm. Use Sunroom, Library, Garden room, or Quiet room to browse one part of the shelter.
The optional Django admin lets your team change a cat's details and status. Visitors see those updates as soon as they return to the directory.
Deploy it
Bring this folder to Tokay any way you like. Paste or upload it in the dashboard, push it with git, or hand it to your AI agent along with our agent instructions. Tokay figures out the rest.
Working in Claude, ChatGPT, VS Code, or another MCP client? Connect the Tokay MCP server and ask your agent to deploy this example. It signs in through your browser, so there is no token to paste.
New to Tokay? The getting started guide walks you through your first deploy.
Generate the required secret
Generate a value on your own computer.
python -c "import secrets; print(secrets.token_urlsafe(50))"
Keep the value ready for the setup step after Tokay creates the Service. Do not put it in this folder.
Finish setup in Tokay
Choose Environment on the directory Service.
- Under From your code, find
SECRET_KEY. Choose Set shared value, paste the value you generated, and save it. Future deployments will use the same encrypted Project Secret. - Find
DATABASE_URLunder From your infrastructure. Confirm the database, leaveappas its name, and save the choice. - Check that both rows say Good to go, then complete the deployment tracker. Use Go Live if prompted. The finished Service's live URL opens the directory.
Try it
- Search for
calm, then open the Library room. - Deploy the same code again after the directory is live.
- Repeat the search. The cats and their adoption status remain in PostgreSQL.
Turn on the admin
Open the Service's Environment page. Find ADMIN_PASSWORD under From your code, choose Set shared value, then deploy again. Sign in at /admin/ with username shelter-admin and the password you set. Change a cat's status, then return to the directory to see the update.
You choose the admin password. The app never generates or prints one in the logs.
How it works
Want to know where the directory comes from? A Django migration defines the database and a small seed step adds or updates the fictional cats. Tokay tests python manage.py migrate --noinput against a copy before the new version receives traffic.
Your CSS stays in Django's normal static file layout. Tokay collects and serves it with the directory, so the live page and its assets ship together.
Secrets
| Name | Required | Purpose |
|---|---|---|
SECRET_KEY |
Yes | Signs Django sessions and security tokens. Store it as an encrypted Project Secret. |
ADMIN_PASSWORD |
No | Creates or updates the opt in shelter-admin account. |
Grow from here
Try the Streamlit support dashboard when the app is only for your own team.
Source code
catadoption/__init__.py Raw
This file is empty.
catadoption/settings.py Raw
import os
from pathlib import Path
from urllib.parse import unquote, urlparse
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = False
ALLOWED_HOSTS = ["*"]
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"cats.apps.CatsConfig",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "catadoption.urls"
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",
],
},
}]
WSGI_APPLICATION = "catadoption.wsgi.application"
database_url = urlparse(os.environ["DATABASE_URL"])
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": database_url.path.lstrip("/"),
"USER": unquote(database_url.username or ""),
"PASSWORD": unquote(database_url.password or ""),
"HOST": database_url.hostname or "",
"PORT": database_url.port or 5432,
}
}
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LANGUAGE_CODE = "en-us"
TIME_ZONE = "America/New_York"
USE_I18N = True
USE_TZ = True
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
catadoption/urls.py Raw
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("cats.urls")),
]
catadoption/wsgi.py Raw
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "catadoption.settings")
application = get_wsgi_application()
cats/migrations/0001_initial.py Raw
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Cat",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=80, unique=True)),
("age", models.PositiveSmallIntegerField()),
("temperament", models.CharField(max_length=160)),
("room", models.CharField(choices=[("sunroom", "Sunroom"), ("library", "Library"), ("garden", "Garden room"), ("quiet", "Quiet room")], max_length=20)),
("status", models.CharField(choices=[("available", "Ready to meet"), ("pending", "Meeting scheduled"), ("adopted", "Heading home")], default="available", max_length=20)),
("note", models.TextField()),
],
options={"ordering": ["name"]},
),
]
cats/migrations/__init__.py Raw
This file is empty.
cats/static/cats/site.css Raw
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, sans-serif; background: #f3eee5; color: #292a25; }
* { box-sizing: border-box; }
body { margin: 0; }
a { color: inherit; }
header { width: min(1180px, calc(100% - 36px)); margin: 0 auto; padding: 24px 0; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #c8bfae; }
.brand { font: 700 1.35rem Georgia, serif; text-decoration: none; }
nav { display: flex; flex-wrap: wrap; gap: 17px; }
nav a { color: #625e53; font-size: 0.88rem; }
main { width: min(1180px, calc(100% - 36px)); margin: 55px auto 90px; }
.intro { display: grid; grid-template-columns: 1.25fr 0.75fr; gap: 20px 70px; align-items: end; }
.kicker { grid-column: 1 / -1; margin: 0; color: #a24e39; font-size: 0.75rem; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; }
h1 { margin: 0; font: 500 clamp(3.8rem, 9vw, 8rem)/0.86 Georgia, serif; letter-spacing: -0.055em; }
.intro > p:not(.kicker) { margin: 0; color: #6b675d; font: 1.05rem/1.7 Georgia, serif; }
form { grid-column: 1 / -1; margin-top: 26px; }
label { display: block; margin-bottom: 8px; font-size: 0.84rem; font-weight: 800; }
form div { display: flex; max-width: 650px; }
input { flex: 1; min-width: 0; padding: 13px 15px; border: 1px solid #aca18f; border-radius: 10px 0 0 10px; background: #fffdf8; font: inherit; }
button { border: 0; border-radius: 0 10px 10px 0; padding: 0 20px; background: #294f43; color: white; font: inherit; font-weight: 800; }
.cats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; margin-top: 46px; }
article { padding: 17px; border: 1px solid #d0c7b7; border-radius: 18px; background: #fbf8f1; }
.portrait { min-height: 210px; display: grid; place-items: center; border-radius: 12px; color: #fffaf0; font: 500 4rem Georgia, serif; letter-spacing: -0.08em; }
.room-sunroom { background: #c57845; } .room-library { background: #526d63; } .room-garden { background: #778a4f; } .room-quiet { background: #6e6581; }
.card-head { display: flex; align-items: baseline; justify-content: space-between; margin-top: 17px; }
h2 { margin: 0; font: 500 2rem Georgia, serif; }
.card-head span { color: #817a6d; }
article p { color: #686358; line-height: 1.55; }
.temperament { min-height: 48px; color: #3f413a; font-weight: 700; }
article footer { display: flex; justify-content: space-between; gap: 10px; padding-top: 13px; border-top: 1px solid #ded7ca; color: #777064; font-size: 0.78rem; }
article footer strong { color: #2d6248; } article footer .pending { color: #936327; } article footer .adopted { color: #755a83; }
.empty { grid-column: 1 / -1; padding: 40px; background: #fbf8f1; border-radius: 16px; }
@media (max-width: 820px) { .intro { grid-template-columns: 1fr; } .cats { grid-template-columns: repeat(2, 1fr); } nav { display: none; } }
@media (max-width: 540px) { .cats { grid-template-columns: 1fr; } .portrait { min-height: 175px; } }
cats/templates/cats/directory.html Raw
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Good Room Cat Shelter</title>
<link rel="stylesheet" href="{% static 'cats/site.css' %}">
</head>
<body>
<header>
<a class="brand" href="/">Good Room</a>
<nav><a href="/rooms/sunroom/">Sunroom</a><a href="/rooms/library/">Library</a><a href="/rooms/garden/">Garden room</a><a href="/rooms/quiet/">Quiet room</a></nav>
</header>
<main>
<section class="intro">
<p class="kicker">{{ room_name }}</p>
<h1>Meet someone good.</h1>
<p>Every cat here has been observed with care. Read slowly, visit twice, and choose the relationship that fits both of you.</p>
<form method="get" action="/">
<label for="q">Search by name or temperament</label>
<div><input id="q" name="q" value="{{ query }}" placeholder="Try calm, playful, or Ada"><button>Search</button></div>
</form>
</section>
<section class="cats">
{% for cat in cats %}
<article>
<div class="portrait room-{{ cat.room }}" aria-hidden="true">{{ cat.initials }}</div>
<div class="card-head"><h2>{{ cat.name }}</h2><span>{{ cat.age }} yr</span></div>
<p class="temperament">{{ cat.temperament }}</p>
<p>{{ cat.note }}</p>
<footer><span>{{ cat.get_room_display }}</span><strong class="{{ cat.status }}">{{ cat.get_status_display }}</strong></footer>
</article>
{% empty %}
<p class="empty">No cats match that search yet. Try a room or a gentler word.</p>
{% endfor %}
</section>
</main>
</body>
</html>
cats/__init__.py Raw
This file is empty.
cats/admin.py Raw
from django.contrib import admin
from .models import Cat
@admin.register(Cat)
class CatAdmin(admin.ModelAdmin):
list_display = ["name", "age", "room", "status"]
list_filter = ["room", "status"]
search_fields = ["name", "temperament", "note"]
cats/apps.py Raw
import sys
import threading
from pathlib import Path
from django.apps import AppConfig
class CatsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "cats"
def ready(self):
executable = Path(sys.argv[0]).name
if "gunicorn" not in executable and "runserver" not in sys.argv:
return
from .seed import seed_with_retry
threading.Thread(target=seed_with_retry, name="cat-seed", daemon=True).start()
cats/models.py Raw
from django.db import models
class Cat(models.Model):
ROOM_CHOICES = [
("sunroom", "Sunroom"),
("library", "Library"),
("garden", "Garden room"),
("quiet", "Quiet room"),
]
STATUS_CHOICES = [
("available", "Ready to meet"),
("pending", "Meeting scheduled"),
("adopted", "Heading home"),
]
name = models.CharField(max_length=80, unique=True)
age = models.PositiveSmallIntegerField()
temperament = models.CharField(max_length=160)
room = models.CharField(max_length=20, choices=ROOM_CHOICES)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="available")
note = models.TextField()
class Meta:
ordering = ["name"]
def __str__(self):
return self.name
@property
def initials(self):
return "".join(part[0] for part in self.name.split()[:2]).upper()
cats/seed.py Raw
import os
import time
from django.contrib.auth import get_user_model
from django.db import OperationalError, ProgrammingError
from .models import Cat
CATS = [
("Ada", 3, "Curious, composed, and fond of high shelves", "library", "available", "Ada likes a slow introduction and settles beside quiet readers."),
("Biscuit", 6, "Warm, chatty, and very food motivated", "sunroom", "available", "Biscuit greets familiar people with a small chirp."),
("Clover", 2, "Playful, quick, and confident", "garden", "pending", "Clover would enjoy a home with room for an evening chase."),
("Dashi", 8, "Gentle, observant, and unhurried", "quiet", "available", "Dashi prefers soft voices and a dependable windowsill."),
("Etta", 4, "Social, tidy, and interested in everything", "sunroom", "adopted", "Etta is preparing to leave for her new home."),
("Fig", 1, "Bold, bouncy, and always carrying a toy", "garden", "available", "Fig still has kitten energy and learns quickly."),
("Gus", 10, "Patient, affectionate, and excellent company", "library", "available", "Gus is happiest close to someone working at a desk."),
("Hazel", 5, "Independent, calm, and softly affectionate", "quiet", "pending", "Hazel chooses her people carefully and then stays close."),
("Iris", 7, "Friendly, adaptable, and fond of brushing", "sunroom", "available", "Iris has lived with another calm cat."),
("Juniper", 2, "Athletic, alert, and eager to play", "garden", "available", "Juniper turns cardboard into an obstacle course."),
("Kumo", 9, "Quiet, steady, and happiest near a blanket", "quiet", "available", "Kumo needs daily medicine hidden in a favorite treat."),
("Linden", 4, "Tender, shy at first, and deeply loyal", "library", "available", "Linden comes forward when the room becomes still."),
("Mabel", 11, "Dignified, sweet, and clear about supper", "sunroom", "adopted", "Mabel found a sunny retirement home."),
("Nori", 3, "Bright, busy, and fascinated by water", "garden", "available", "Nori likes puzzle feeders and a sturdy scratching post."),
("Olive", 6, "Easygoing, affectionate, and quietly comic", "library", "available", "Olive is an experienced lap cat."),
("Pip", 1, "Small, brave, and ready for a patient teacher", "garden", "pending", "Pip is learning that hands bring good things."),
("Quincy", 5, "Polite, thoughtful, and devoted to routine", "quiet", "available", "Quincy enjoys one person conversations."),
("Romy", 4, "Confident, expressive, and always nearby", "sunroom", "available", "Romy wants to supervise the household."),
("Sable", 8, "Soft spoken, affectionate, and settled", "library", "available", "Sable would suit a calm home beautifully."),
("Tansy", 2, "Cheerful, nimble, and generous with head bumps", "garden", "available", "Tansy is ready for play and company."),
]
def seed_once():
for name, age, temperament, room, status, note in CATS:
Cat.objects.update_or_create(
name=name,
defaults={"age": age, "temperament": temperament, "room": room, "status": status, "note": note},
)
# The admin is opt in. No password is generated or printed.
admin_password = os.getenv("ADMIN_PASSWORD", "").strip()
if admin_password:
user, _created = get_user_model().objects.update_or_create(
username="shelter-admin",
defaults={"email": "admin@example.test", "is_staff": True, "is_superuser": True},
)
user.set_password(admin_password)
user.save(update_fields=["password"])
def seed_with_retry():
delay = 1
for attempt in range(8):
try:
seed_once()
return
except (OperationalError, ProgrammingError):
if attempt == 7:
return
time.sleep(delay)
delay = min(delay * 2, 8)
cats/urls.py Raw
from django.urls import path
from . import views
urlpatterns = [
path("", views.directory, name="directory"),
path("rooms/<slug:room_slug>/", views.room, name="room"),
path("health", views.health, name="health"),
]
cats/views.py Raw
from django.db.models import Q
from django.http import JsonResponse
from django.shortcuts import render
from .models import Cat
def directory(request):
query = request.GET.get("q", "").strip()
cats = Cat.objects.all()
if query:
cats = cats.filter(
Q(name__icontains=query)
| Q(temperament__icontains=query)
| Q(note__icontains=query)
| Q(room__icontains=query)
)
return render(request, "cats/directory.html", {"cats": cats, "query": query, "room_name": "All rooms"})
def room(request, room_slug):
room_names = dict(Cat.ROOM_CHOICES)
cats = Cat.objects.filter(room=room_slug)
return render(request, "cats/directory.html", {"cats": cats, "query": "", "room_name": room_names.get(room_slug, "Room")})
def health(_request):
return JsonResponse({"ok": True})
manage.py Raw
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "catadoption.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
requirements.txt Raw
Django==6.0.7
gunicorn==26.0.0
psycopg[binary]==3.3.4
Built from revision 52c7b7f