# pick a folder
mkdir mysite && cd mysite
# create & activate venv (Windows)
python -m venv .venv
.venv\Scripts\activate
# macOS/Linux:
# python3 -m venv .venv
# source .venv/bin/activatepip install --upgrade pip
pip install django psycopg[binary] python-decouple gunicorn whitenoise
Why:
psycopg for PostgreSQL (optional now, useful later).python-decouple for .env variables.gunicorn/whitenoise for production.django-admin startproject config .
Tree now:
mysite/
config/
__init__.py
asgi.py
settings.py
urls.py
wsgi.py
manage.py
python manage.py runserver
Open http://127.0.0.1:8000 — you should see “The install worked!”
python manage.py startapp blog
Add blog to INSTALLED_APPS in config/settings.py.
blog/models.py
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
body = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts")
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
published = models.BooleanField(default=True)
class Meta:
ordering = ["-created"]
def __str__(self):
return self.title
Apply migrations:
python manage.py makemigrations
python manage.py migrate
Create a superuser:
python manage.py createsuperuser
blog/admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ("title", "author", "published", "created")
prepopulated_fields = {"slug": ("title",)}
search_fields = ("title", "body")
list_filter = ("published", "created")
Create app URLs:blog/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("", views.post_list, name="post_list"),
path("<slug:slug>/", views.post_detail, name="post_detail"),
]
Wire them in project URLs:config/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("blog.urls")),
]
Now visit http://127.0.0.1:8000/admin and add a Post.
Views:blog/views.py
from django.shortcuts import get_object_or_404, render
from .models import Post
def post_list(request):
posts = Post.objects.filter(published=True)
return render(request, "blog/post_list.html", {"posts": posts})
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug, published=True)
return render(request, "blog/post_detail.html", {"post": post})
Templates:
Create templates/blog/post_list.html:
<!doctype html>
<html>
<head><meta charset="utf-8"><title>Blog</title></head>
<body>
<h1>Posts</h1>
<ul>
{% for p in posts %}
<li><a href="{{ p.get_absolute_url|default:'/'|add:p.slug|add:'/' }}">{{ p.title }}</a></li>
{% empty %}
<li>No posts yet.</li>
{% endfor %}
</ul>
</body>
</html>
Create templates/blog/post_detail.html:
<!doctype html>
<html>
<head><meta charset="utf-8"><title>{{ post.title }}</title></head>
<body>
<a href="/">← Back</a>
<h1>{{ post.title }}</h1>
<p><small>By {{ post.author }} — {{ post.created|date:"Y-m-d H:i" }}</small></p>
<div>{{ post.body|linebreaks }}</div>
</body>
</html>
Tell Django where templates live:config/settings.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATES[0]["DIRS"] = [BASE_DIR / "templates"]
config/settings.py
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"] # for dev
STATIC_ROOT = BASE_DIR / "staticfiles" # for collectstatic (prod)
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
During development:config/urls.py
from django.conf import settings
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Create folders:
mkdir static media templates
mkdir templates/blog static/css static/js
Navigate to http://127.0.0.1:8000/ you should see the published posts.