Skip to content
Merged
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
7 changes: 7 additions & 0 deletions python/django-notes/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
db.sqlite3
__pycache__/
*.pyc
.env
.env.local
.vercel
.env*.local
47 changes: 47 additions & 0 deletions python/django-notes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
name: Django Notes
slug: django-notes
description: A simple note-taking app built with Django, using SQLite locally and Postgres on Vercel.
framework: Django
useCase: Starter
database: Postgres
deployUrl: https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fpython%2Fdjango-notes&project-name=django-notes&repository-name=django-notes&demo-title=Django%20Notes&demo-description=A%20simple%20note-taking%20app%20built%20with%20Django.&demo-url=https%3A%2F%2Fdjango-notes.vercel.app%2F&products=%5B%7B%22type%22%3A%22integration%22%2C%22group%22%3A%22postgres%22%7D%5D
demoUrl: https://django-notes.vercel.app/
relatedTemplates:
- django
- django-rest-framework
---

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fpython%2Fdjango-notes&project-name=django-notes&repository-name=django-notes&demo-title=Django%20Notes&demo-description=A%20simple%20note-taking%20app%20built%20with%20Django.&demo-url=https%3A%2F%2Fdjango-notes.vercel.app%2F&products=%5B%7B%22type%22%3A%22integration%22%2C%22group%22%3A%22postgres%22%7D%5D)

# Django Notes

A simple note-taking app built with Django, 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

This example uses Django's ORM, templates, and `staticfiles` app, served via WSGI on Vercel. The database defaults to SQLite locally and switches to Postgres when `DATABASE_URL` is set — Vercel automatically provisions this environment variable when you add a Postgres database. You can add a database during project import or later from the Storage tab.

Migrations run automatically on each deploy via `[tool.vercel.scripts]` in `pyproject.toml`. Because of this, you should use a database provider that supports branching (like Neon) so that preview deployments run migrations against an isolated branch rather than your production database.

Setting `READ_ONLY=true` disables all write operations.

## 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`.

## One-Click Deploy

Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples):

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fpython%2Fdjango-notes&project-name=django-notes&repository-name=django-notes&demo-title=Django%20Notes&demo-description=A%20simple%20note-taking%20app%20built%20with%20Django.&demo-url=https%3A%2F%2Fdjango-notes.vercel.app%2F&products=%5B%7B%22type%22%3A%22integration%22%2C%22group%22%3A%22postgres%22%7D%5D)
71 changes: 71 additions & 0 deletions python/django-notes/config/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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 = os.environ.get("DJANGO_DEBUG") == "1"

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

READ_ONLY = os.environ.get("READ_ONLY") == "1"

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

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

ROOT_URLCONF = "config.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 = "config.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_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "/static/"
5 changes: 5 additions & 0 deletions python/django-notes/config/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/config/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
WSGI config for django-notes 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/stable/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.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", "config.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 %} — Vercel + Django</title>
<link rel="stylesheet" href="{% static 'notes/style.css' %}">
</head>
<body>
<header>
<a href="/">Notes <span style="color:#888">using</span> Vercel + Django</a>
</header>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>
24 changes: 24 additions & 0 deletions python/django-notes/notes/templates/notes/detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% extends "notes/base.html" %}

{% block title %}{{ note.title }}{% endblock %}

{% block content %}
<div class="actions">
<h1>{{ note.title }}</h1>
{% if not read_only %}
<a href="{% url 'note_edit' note.pk %}" class="btn" style="margin-left: auto">Edit</a>
{% endif %}
</div>

<div class="card">
<p style="white-space: pre-wrap; line-height: 1.6;">{{ note.body }}</p>
</div>

{% if not read_only %}
<form method="post" action="{% url 'note_delete' note.pk %}" style="margin-top: 1.5rem">
{% csrf_token %}
<button type="submit" class="btn btn-danger"
onclick="return confirm('Delete this note?')">Delete</button>
</form>
{% endif %}
{% endblock %}
Loading