Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions python/django-notes/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
db.sqlite3
__pycache__/
*.pyc
.env
.env.local
.vercel
90 changes: 90 additions & 0 deletions python/django-notes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Django Notes

A simple note-taking app built with Django 6, demonstrating server-side rendering, URL routing, forms, and ORM with SQLite locally and Postgres on Vercel.

## Demo

https://django-notes.vercel.app/

## How it Works

The `Note` model is defined in `notes/models.py`:

```python
class Note(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
```

The database defaults to SQLite locally and switches to Postgres when `DATABASE_URL` is present — Vercel sets this automatically when you provision a Postgres database:

```python
if os.environ.get("DATABASE_URL"):
# Postgres on Vercel
else:
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", ...}}
```

CSS is served as a static file from `notes/static/notes/style.css` via Django's `staticfiles` app and referenced in the base template with `{% static %}`.

The app is exposed to Vercel via WSGI in `app/wsgi.py`:

```python
application = get_wsgi_application()
```

Setting the environment variable `READ_ONLY=true` disables all write operations and hides the create, edit, and delete UI.

## Running Locally

```bash
uv sync
uv run python manage.py migrate
uv run python manage.py runserver
```

Your app is now available at `http://localhost:8000`.

## Deploying to Vercel

This app uses SQLite locally. Vercel's serverless environment does not have a persistent filesystem, so you'll need to provision a Postgres database before deploying.

**1. Install the Vercel CLI and link your project**

```bash
npm install -g vercel
vercel link
```

**2. Add a Postgres database**

From the [Vercel dashboard](https://vercel.com/dashboard), go to your project's **Storage** tab and create a Postgres database. Vercel will automatically add `DATABASE_URL` to your project's environment variables.

**3. Pull environment variables and run migrations**

```bash
vercel env pull
uv run python manage.py migrate
```

**4. Deploy**

```bash
vercel --prod
```

## Project Structure

```
django-notes/
├── app/ # Django project config (settings, urls, wsgi)
└── notes/ # Notes app
├── models.py # Note model (title, body)
├── forms.py # NoteForm
├── views.py # list, create, detail, edit, delete views
├── urls.py # URL patterns
├── migrations/ # Database migrations
├── static/notes/ # CSS
└── templates/notes/ # HTML templates
```

File renamed without changes.
70 changes: 70 additions & 0 deletions python/django-notes/app/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import os
import urllib.parse
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "django-insecure-change-me-in-production")

DEBUG = True

ALLOWED_HOSTS = ["127.0.0.1", "localhost", ".vercel.app"]

READ_ONLY = os.environ.get("READ_ONLY", "").lower() in ("1", "true", "yes")

INSTALLED_APPS = [
"django.contrib.contenttypes",
"django.contrib.staticfiles",
"notes",
]

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
]

ROOT_URLCONF = "app.urls"

TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"notes.context_processors.read_only",
],
},
},
]

WSGI_APPLICATION = "app.wsgi.application"

if os.environ.get("DATABASE_URL"):
url = urllib.parse.urlparse(os.environ["DATABASE_URL"])
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": url.path.lstrip("/"),
"USER": url.username,
"PASSWORD": url.password,
"HOST": url.hostname,
"PORT": url.port,
}
}
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True

STATIC_URL = "/static/"
5 changes: 5 additions & 0 deletions python/django-notes/app/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.urls import path, include

urlpatterns = [
path("", include("notes.urls")),
]
16 changes: 16 additions & 0 deletions python/django-notes/app/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
WSGI config for app project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")

application = get_wsgi_application()
33 changes: 33 additions & 0 deletions python/django-notes/manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""

import os
import sys

# Load .env.local if present (e.g. after running `vercel env pull`)
_env_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env.local")
if os.path.exists(_env_file):
with open(_env_file) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, _, value = line.partition("=")
os.environ.setdefault(key.strip(), value.strip().strip("\"'"))


def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)


if __name__ == "__main__":
main()
Empty file.
5 changes: 5 additions & 0 deletions python/django-notes/notes/context_processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.conf import settings


def read_only(request):
return {"read_only": settings.READ_ONLY}
18 changes: 18 additions & 0 deletions python/django-notes/notes/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django import forms
from .models import Note


class NoteForm(forms.ModelForm):
class Meta:
model = Note
fields = ["title", "body"]
widgets = {
"title": forms.TextInput(attrs={
"placeholder": "Note title",
"autofocus": True,
}),
"body": forms.Textarea(attrs={
"placeholder": "Write your note here...",
"rows": 8,
}),
}
27 changes: 27 additions & 0 deletions python/django-notes/notes/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.db import migrations, models


def seed_initial_note(apps, schema_editor):
Note = apps.get_model("notes", "Note")
Note.objects.create(title="Hello", body="world!")
Note.objects.create(title="Try it yourself", body="This demo is read-only. Deploy your own copy to create, edit, and delete notes.")
Note.objects.create(title="Django on Vercel", body="This app runs on Vercel Serverless Functions using the Python runtime.")


class Migration(migrations.Migration):

initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="Note",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("title", models.CharField(max_length=200)),
("body", models.TextField()),
],
),
migrations.RunPython(seed_initial_note, migrations.RunPython.noop),
]
Empty file.
9 changes: 9 additions & 0 deletions python/django-notes/notes/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.db import models


class Note(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()

def __str__(self):
return self.title
89 changes: 89 additions & 0 deletions python/django-notes/notes/static/notes/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #000;
color: #fff;
min-height: 100vh;
}

header {
border-bottom: 1px solid #333;
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
}

header a { color: #fff; text-decoration: none; font-weight: 600; }
header nav a { font-size: 0.875rem; color: #888; }
header nav a:hover { color: #fff; }

main { max-width: 800px; margin: 0 auto; padding: 2rem; }

h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 1.5rem; }

.btn {
display: inline-block;
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
cursor: pointer;
border: 1px solid #333;
background: #111;
color: #fff;
}
.btn:hover { background: #222; border-color: #555; }
.btn-primary { background: #fff; color: #000; border-color: #fff; }
.btn-primary:hover { background: #eee; }
.btn-danger { background: #c00; border-color: #c00; }
.btn-danger:hover { background: #a00; border-color: #a00; }

.card {
background: #111;
border: 1px solid #333;
border-radius: 8px;
padding: 1.25rem;
}

.note-grid { display: grid; gap: 1rem; }

.note-card {
background: #111;
border: 1px solid #333;
border-radius: 8px;
padding: 1.25rem;
text-decoration: none;
color: inherit;
display: block;
transition: border-color 0.15s;
}
.note-card:hover { border-color: #555; }
.note-card h2 { font-size: 1rem; font-weight: 600; margin-bottom: 0.5rem; }
.note-card p { font-size: 0.875rem; color: #888; }

.actions { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; align-items: center; }

label { display: block; font-size: 0.875rem; color: #888; margin-bottom: 0.5rem; }

input[type="text"], textarea {
width: 100%;
background: #111;
border: 1px solid #333;
border-radius: 6px;
padding: 0.625rem 0.75rem;
color: #fff;
font-size: 0.875rem;
font-family: inherit;
margin-bottom: 1rem;
}
input[type="text"]:focus, textarea:focus {
outline: none;
border-color: #555;
}

.errorlist { color: #f55; font-size: 0.8rem; margin-bottom: 0.5rem; list-style: none; }

.empty { color: #555; text-align: center; padding: 3rem 0; }
18 changes: 18 additions & 0 deletions python/django-notes/notes/templates/notes/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Django Notes{% endblock %}</title>
<link rel="stylesheet" href="{% static 'notes/style.css' %}">
</head>
<body>
<header>
<a href="/">Django Notes</a>
</header>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>
Loading