diff --git a/backend/.dockerignore b/backend/.dockerignore index 80b0b8c..a375c52 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -1,3 +1,4 @@ venv migrations +zapp.db diff --git a/backend/Dockerfile b/backend/Dockerfile index fdeff17..2145c54 100755 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,7 +1,11 @@ # syntax=docker/dockerfile:1.4 FROM python:3-slim-bookworm AS builder -RUN apt update && apt install -y libmariadb-dev gcc +# Second line optional/debug/qol +RUN apt update && apt install -y \ + libmariadb-dev gcc \ + mariadb-client + WORKDIR /code COPY requirements.txt /code diff --git a/backend/README.md b/backend/README.md index a7b6c04..e2b34d0 100644 --- a/backend/README.md +++ b/backend/README.md @@ -22,6 +22,10 @@ Prod only, require sys packages: pip install mariadb pip install uwsgi ... +``` + +Freeze/requirements.txt. Better to audit this inside python:3-bookworm-slim container. +``` pip freeze > requirements.txt ``` diff --git a/backend/app/models.py b/backend/app/models.py index dc5a1a3..9cace57 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -9,6 +9,13 @@ import pydenticon, hashlib, base64 from app import db, login from flask_login import UserMixin +followers = sa.Table( + 'followers', + db.metadata, + sa.Column('follower_id', sa.Integer, sa.ForeignKey('user.id'), primary_key=True), + sa.Column('followed_id', sa.Integer, sa.ForeignKey('user.id'), primary_key=True) +) + class User(UserMixin, db.Model): id: so.Mapped[int] = so.mapped_column(primary_key=True) username: so.Mapped[str] = so.mapped_column(sa.String(64), index=True, unique=True) @@ -17,6 +24,16 @@ class User(UserMixin, db.Model): posts: so.WriteOnlyMapped['Post'] = so.relationship(back_populates='author') about_me: so.Mapped[Optional[str]] = so.mapped_column(sa.String(140)) last_seen: so.Mapped[Optional[datetime]] = so.mapped_column(default=lambda: datetime.now(timezone.utc)) + following: so.WriteOnlyMapped['User'] = so.relationship( + secondary=followers, + primaryjoin=(followers.c.follower_id == id), + secondaryjoin=(followers.c.followed_id == id), + back_populates='followers') + followers: so.WriteOnlyMapped['User'] = so.relationship( + secondary=followers, + primaryjoin=(followers.c.followed_id == id), + secondaryjoin=(followers.c.follower_id == id), + back_populates='following') def set_password(self, password): self.password_hash = generate_password_hash(password) @@ -49,6 +66,36 @@ class User(UserMixin, db.Model): pngloc = os.path.join(basedir, 'usercontent', 'identicon', digest + '.png') return pngloc + def follow(self, user): + if not self.is_following(user): + self.following.add(user) + def unfollow(self, user): + if self.is_following(user): + self.following.remove(user) + def is_following(self, user): + query = self.following.select().where(User.id == user.id) + return db.session.scalar(query) is not None + def followers_count(self): + query = sa.select(sa.func.count()).select_from(self.followers.select().subquery()) + return db.session.scalar(query) + def following_count(self): + query = sa.select(sa.func.count()).select_from(self.following.select().subquery()) + return db.session.scalar(query) + def following_posts(self): + Author = so.aliased(User) + Follower = so.aliased(User) + return ( + sa.select(Post) + .join(Post.author.of_type(Author)) + .join(Author.followers.of_type(Follower), isouter=True) + .where(sa.or_( + Follower.id == self.id + Author.id == self.id + )) + .group_by(Post) + .order_by(Post.timestamp.desc()) + ) + def __repr__(self): return ''.format(self.username) diff --git a/backend/dbdb.sh b/backend/dbdb.sh new file mode 100644 index 0000000..4c3930b --- /dev/null +++ b/backend/dbdb.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Okay to publish -- creds are local dev only + +mariadb -hdb -uflasku -pflaskp flask diff --git a/backend/requirements.txt b/backend/requirements.txt index 5df5030..2cdaf64 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -21,5 +21,9 @@ SQLAlchemy==2.0.31 typing_extensions==4.12.2 Werkzeug==3.0.3 WTForms==3.1.2 -uwsgi -mariadb +mariadb==1.1.10 +packaging==24.1 +setuptools==72.1.0 +uWSGI==2.0.26 +wheel==0.43.0 + diff --git a/backend/tests.py b/backend/tests.py new file mode 100644 index 0000000..1b31bb1 --- /dev/null +++ b/backend/tests.py @@ -0,0 +1,5 @@ +import os +os.environ['DATABASE_URL'] = 'sqlite://' + +from datetime import datetime, timezone, timedelta + diff --git a/compose.yaml b/compose.yaml index 5e45c24..a4ffbc3 100644 --- a/compose.yaml +++ b/compose.yaml @@ -28,7 +28,10 @@ services: build: context: backend target: builder - restart: always + # Next two are only debug, used without restart + stdin_open: true + tty: true + #restart: always # Comment following line to use flask (1worker, dev), uncomment to use uwsgi (wsgi) #command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "microblog:app"] environment: