Erstelle dein eigenes Forum mit FastAPI: Schritt 6 – Kommentare und Antworten
Grace Collins
Solutions Engineer · Leapcell

Im vorherigen Artikel haben wir die Funktion zur Bearbeitung von Beiträgen zu unserem Forum hinzugefügt, die es den Benutzern ermöglicht, ihre veröffentlichten Inhalte zu ändern.
Neben dem Veröffentlichen von Beiträgen ist Interaktion für ein Forum unerlässlich. Wenn Benutzer einen interessanten (oder kontroversen) Beitrag sehen, möchten sie ihre Meinung äußern.
In diesem Artikel werden wir eine Interaktionsfunktion zu unserem Forum hinzufügen: die Implementierung von Beitrags-Kommentaren und Antworten, die es den Benutzern ermöglicht, Diskussionen rund um Beiträge zu führen.
Schritt 1: Aktualisiere das Datenbankmodell
Wir benötigen eine neue Tabelle zur Speicherung von Kommentaren. Darüber hinaus müssen Kommentare selbst Antworten unterstützen, um eine hierarchische Struktur zu bilden.
Füge in models.py das Comment-Modell hinzu und aktualisiere die Modelle User und Post, um Beziehungen herzustellen.
models.py (Aktualisiert)
from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship from database import Base class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True, index=True) hashed_password = Column(String) posts = relationship("Post", back_populates="owner", cascade="all, delete-orphan") comments = relationship("Comment", back_populates="owner", cascade="all, delete-orphan") class Post(Base): __tablename__ = "posts" id = Column(Integer, primary_key=True, index=True) title = Column(String, index=True) content = Column(String) owner_id = Column(Integer, ForeignKey("users.id")) owner = relationship("User", back_populates="posts") comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan") class Comment(Base): __tablename__ = "comments" id = Column(Integer, primary_key=True, index=True) content = Column(String, nullable=False) post_id = Column(Integer, ForeignKey("posts.id")) owner_id = Column(Integer, ForeignKey("users.id")) parent_id = Column(Integer, ForeignKey("comments.id"), nullable=True) owner = relationship("User", back_populates="comments") post = relationship("Post", back_populates="comments") # Self-referencing relationship for replies parent = relationship("Comment", back_populates="replies", remote_side=[id]) replies = relationship("Comment", back_populates="parent", cascade="all, delete-orphan")
Hier sind die wichtigsten Änderungen, die wir vorgenommen haben:
- Das
Comment-Modell wurde erstellt und überpost_idbzw.owner_idmit Beiträgen und Benutzern verknüpft. Das Feldparent_idverweist auf dieideines anderen Kommentars. Wenn esNULList, ist es ein Top-Level-Kommentar; andernfalls ist es eine Antwort. - Die Modelle
UserundPostwurden aktualisiert und mit einer Beziehung zuCommentergänzt.cascade="all, delete-orphan"stellt sicher, dass beim Löschen eines Benutzers oder eines Beitrags auch die zugehörigen Kommentare gelöscht werden.
Erstelle als Nächstes diese neue Tabelle in deiner Datenbank. Die entsprechende SQL-Anweisung lautet:
CREATE TABLE comments ( id SERIAL PRIMARY KEY, content TEXT NOT NULL, post_id INTEGER NOT NULL, owner_id INTEGER NOT NULL, parent_id INTEGER, CONSTRAINT fk_post FOREIGN KEY(post_id) REFERENCES posts(id) ON DELETE CASCADE, CONSTRAINT fk_owner FOREIGN KEY(owner_id) REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT fk_parent FOREIGN KEY(parent_id) REFERENCES comments(id) ON DELETE CASCADE );
Wenn deine Datenbank mit Leapcell erstellt wurde,
kannst du diese SQL-Anweisungen direkt in seinem webbasierten Bedienfeld ausführen.

Schritt 2: Erstelle die Beitragsdetailseite und den Kommentarbereich
Derzeit werden alle Beiträge auf der Homepage angezeigt. Um Platz für einen Kommentarbereich zu schaffen, müssen wir für jeden Beitrag eine separate Detailseite erstellen.
Erstelle zuerst eine neue Datei post_detail.html im Ordner templates.
templates/post_detail.html
<!DOCTYPE html> <html> <head> <title>{{ post.title }} - Mein FastAPI Forum</title> <style> body { font-family: sans-serif; margin: 2em; } .post-container { border: 1px solid #ccc; padding: 20px; margin-bottom: 20px; } .comment-form { margin-top: 20px; } .comments-section { margin-top: 30px; } .comment { border-left: 3px solid #eee; padding-left: 15px; margin-bottom: 15px; } .comment .meta { font-size: 0.9em; color: #666; } .replies { margin-left: 30px; } </style> </head> <body> <div class="post-container"> <h1>{{ post.title }}</h1> <p>{{ post.content }}</p> <small>Autor: {{ post.owner.username }}</small> </div> <hr /> <div class="comment-form"> <h3>Hinterlasse einen Kommentar</h3> {% if current_user %} <form action="/posts/{{ post.id }}/comments" method="post"> <textarea name="content" rows="4" style="width:100%;" placeholder="Schreibe deinen Kommentar..." required></textarea><br /> <button type="submit">Absenden</button> </form> {% else %} <p><a href="/login">Einloggen</a>, um einen Kommentar zu schreiben.</p> {% endif %} </div> <div class="comments-section"> <h2>Kommentare</h2> {% for comment in comments %} {% if not comment.parent_id %} <div class="comment"> <p>{{ comment.content }}</p> <p class="meta">Verfasst von {{ comment.owner.username }}</p> {% if current_user %} <form action="/posts/{{ post.id }}/comments" method="post" style="margin-left: 20px;"> <input type="hidden" name="parent_id" value="{{ comment.id }}" /> <textarea name="content" rows="2" style="width:80%;" placeholder="Antworten..." required></textarea><br /> <button type="submit">Antworten</button> </form> {% endif %} <div class="replies"> {% for reply in comment.replies %} <div class="comment"> <p>{{ reply.content }}</p> <p class="meta">Antwortet von {{ reply.owner.username }}</p> </div> {% endfor %} </div> </div> {% endif %} {% endfor %} </div> <a href="/posts">Zurück zur Startseite</a> </body> </html>
Diese Vorlage enthält die Details des Beitrags, ein Formular zum Verfassen neuer Kommentare und einen Bereich zur Anzeige aller Kommentare. Der Einfachheit halber zeigen wir derzeit nur eine Ebene von Antworten an.
Schritt 3: Implementiere die Backend-Routenlogik
Als Nächstes fügen wir in main.py neue Routen hinzu, um die Anzeige der Beitragsdetailseite und das Absenden von Kommentaren zu verarbeiten.
main.py (Neue Routen hinzufügen)
# ... (vorherige Importe bleiben unverändert) ... from sqlalchemy.orm import selectinload # ... (vorheriger Code bleibt unverändert) ... # --- Routen --- # ... (vorherige Routen /, /posts, /api/posts usw. bleiben unverändert) ... @app.get("/posts/{post_id}", response_class=HTMLResponse) async def view_post_detail( request: Request, post_id: int, db: AsyncSession = Depends(get_db), current_user: Optional[models.User] = Depends(get_current_user) ): # Suche nach dem Beitrag result = await db.execute(select(models.Post).where(models.Post.id == post_id).options(selectinload(models.Post.owner))) post = result.scalar_one_or_none() if not post: raise HTTPException(status_code=404, detail="Beitrag nicht gefunden") # Suche nach Kommentaren und lade Autor- und Antworterinformationen vor # Verwende selectinload, um N+1-Abfragen zu vermeiden comment_result = await db.execute( select(models.Comment) .where(models.Comment.post_id == post_id) .options(selectinload(models.Comment.owner), selectinload(models.Comment.replies).selectinload(models.Comment.owner)) .order_by(models.Comment.id) ) comments = comment_result.scalars().all() return templates.TemplateResponse("post_detail.html", { "request": request, "post": post, "comments": comments, "current_user": current_user }) @app.post("/posts/{post_id}/comments") async def create_comment( post_id: int, content: str = Form(...), parent_id: Optional[int] = Form(None), db: AsyncSession = Depends(get_db), current_user: Optional[models.User] = Depends(get_current_user) ): if not current_user: return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) new_comment = models.Comment( content=content, post_id=post_id, owner_id=current_user.id, parent_id=parent_id ) db.add(new_comment) await db.commit() return RedirectResponse(url=f"/posts/{post_id}", status_code=status.HTTP_303_SEE_OTHER) # ... (nachfolgende Routen /posts/{post_id}/edit, /register, /login, /logout usw. bleiben unverändert) ...
Wir haben zwei neue Routen hinzugefügt:
GET /posts/{post_id}: Sucht den Beitrag anhand vonpost_idin der Datenbank und fragt alle zugehörigen Kommentare ab. Schließlich rendert es die Vorlagepost_detail.htmlund übergibt den Beitrag, die Kommentare und die Informationen zum aktuellen Benutzer.POST /posts/{post_id}/comments: Diese Route verarbeitet das Absenden von Kommentaren und Antworten, erstellt einComment-Objekt und speichert es in der Datenbank. Sie empfängtcontentund eine optionaleparent_idaus dem Formular. Wennparent_idvorhanden ist, handelt es sich um eine Antwort.
Schritt 4: Füge einen Einstiegspunkt auf der Homepage hinzu
Alles ist bereit, wir brauchen nur noch einen Einstiegspunkt von der Homepage zur Beitragsdetailseite. Modifiziere templates/posts.html, um den Beitragstitel in einen Link zu verwandeln.
Wir müssen nur <h3>{{ post.title }}</h3> mit einem <a>-Tag umschließen, das auf /posts/{{ post.id }} verlinkt.
templates/posts.html (Aktualisiert)
... (Dateikopf und Stylesheet bleiben unverändert) ... <body> ... (Header- und Beitragsformularabschnitte bleiben unverändert) ... <hr /> <h2>Beitragsliste</h2> {% for post in posts %} <div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px;"> <a href="/posts/{{ post.id }}"><h3>{{ post.title }}</h3></a> <p>{{ post.content }}</p> <small>Autor: {{ post.owner.username if post.owner else 'Unbekannt' }}</small> {% if current_user and post.owner_id == current_user.id %} <div style="margin-top: 10px;"> <a href="/posts/{{ post.id }}/edit">Bearbeiten</a> </div> {% endif %} </div> {% endfor %} </body> </html>
Ausführen und Überprüfen
Starte deinen uvicorn-Server neu:
uvicorn main:app --reload
Besuche http://127.0.0.1:8000 und logge dich ein. Auf der Homepage wirst du feststellen, dass alle Beitragstitel jetzt klickbare Links sind.

Klicke auf einen beliebigen Beitragstitel, und die Seite wird zur Detailseite dieses Beitrags weitergeleitet.
Gib Inhalt in das Kommentarfeld am Ende der Detailseite ein und klicke auf 'Absenden'. Nach der Aktualisierung der Seite erscheint dein Kommentar im Kommentarbereich.

Unter dem Kommentar befindet sich ein kleineres Antwortfeld. Gib dort Inhalt ein und sende ihn ab, und du siehst einen Kommentar als Antwort.

Zusammenfassung
Das Forum unterstützt nun Kommentar- und Antwortfunktionen, die es Benutzern ermöglichen, miteinander zu interagieren.
Während die Funktionalität des Forums komplexer wird, wird die Aufrechterhaltung der Community-Ordnung immer wichtiger. Wie kontrollieren wir, was ein Benutzer tun kann?
Im nächsten Artikel werden wir die Berechtigungsverwaltung einführen. Durch Systeme wie Administratoren werden wir die gesunde Entwicklung der Community sicherstellen. Zum Beispiel das Verbot eines Benutzers zum Sprechen.

