Goal for this part
Move Westloop onto PostgreSQL and define the social graph: Profile, Post, Follow, Like, and Comment. From here on, every snapshot talks to a real database — the same engine you will run on the VPS.
Why PostgreSQL, not SQLite or MongoDB
A follow is a unique pair of user IDs. A like is a unique pair of user and post. Comments belong to a post and an author. PostgreSQL enforces those rules with foreign keys and unique constraints. SQLite locks the whole file when two Gunicorn workers write. MongoDB would push referential integrity into application code you will get wrong under load.
Locally we run Postgres in Docker on host port 5433 so it does not fight a system Postgres on 5432. Each snapshot uses its own database name.
Walkthrough: models
# accounts/models.py
from django.conf import settings
from django.db import models
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
display_name = models.CharField(max_length=80)
bio = models.TextField(blank=True)
neighborhood = models.CharField(max_length=80, blank=True)
avatar = models.ImageField(upload_to="avatars/", blank=True)
def __str__(self):
return self.display_name or self.user.username# posts/models.py
from django.conf import settings
from django.db import models
class Post(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="posts")
body = models.TextField(max_length=500)
slug = models.SlugField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
class Like(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="likes")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("user", "post")
class Comment(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
body = models.CharField(max_length=280)
created_at = models.DateTimeField(auto_now_add=True)# social/models.py
from django.conf import settings
from django.db import models
class Follow(models.Model):
follower = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="following"
)
following = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="followers"
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("follower", "following")
constraints = [
models.CheckConstraint(
check=~models.Q(follower=models.F("following")),
name="no_self_follow",
)
]Point Django at Postgres with dj-database-url so the VPS can inject DATABASE_URL later without editing Python.
import dj_database_url
DATABASES = {
"default": dj_database_url.config(
default="postgres://westloop:westloop@127.0.0.1:5433/westloop_04",
conn_max_age=60,
)
}How to run it and what you should see
From the repo root, start Docker Compose, then migrate this snapshot. python manage.py seed creates Ada and Grace (password password123) plus a few posts. Open Django admin and confirm profiles, posts, and a follow row exist.
Common mistakes
ImageField needs Pillow. Connection refused on 5433 means Compose is not up. unique_together does not stop self-follows — that is why the check constraint exists. Never commit a .env with production credentials.
Try this
In python manage.py shell, make Ada follow Grace, then try the reverse follow and a duplicate follow. The second insert of the same pair should raise an integrity error.