Erstellen Sie Ihr eigenes Forum mit FastAPI: Schritt 10 – Kategorien
Emily Parker
Product Engineer · Leapcell

Im vorherigen Artikel haben wir unserem Forum eine Bild-Upload-Funktion hinzugefügt und so den Inhalt unserer Beiträge bereichert.
Derzeit sind alle Beiträge auf dem gleichen Startseiten-Feed versammelt. Mit wachsendem Inhalt des Forums wird dies sehr unübersichtlich. Benutzer sind möglicherweise nur an bestimmten Themen interessiert, werden aber durch irrelevante Inhalte abgelenkt.
Um dieses Problem zu lösen, werden wir in diesem Artikel eine Kategorien-Funktion einführen. Wir werden verschiedene Bereiche erstellen (z. B. "Technische Diskussion", "Allgemeiner Chat"), die es den Benutzern ermöglichen, bei der Erstellung eines Beitrags eine Kategorie auszuwählen und Beiträge nach Kategorie zu durchsuchen.
Schritt 1: Datenbankmodelle aktualisieren
Wir benötigen eine neue Tabelle categories, um Kategorieinformationen zu speichern, und wir müssen einen Fremdschlüssel in der Tabelle posts hinzufügen, um sie zu verknüpfen.
Öffnen Sie models.py, fügen Sie das Modell Category hinzu und aktualisieren Sie das Modell Post.
models.py (Aktualisiert)
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean from sqlalchemy.orm import relationship from sqlalchemy.dialects.postgresql import TSVECTOR from database import Base # ... (User- und Comment-Modelle bleiben unverändert) ... 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")) image_url = Column(String, nullable=True) # --- Neues Feld --- category_id = Column(Integer, ForeignKey("categories.id"), nullable=False) # --------------- owner = relationship("User", back_populates="posts") comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan") search_vector = Column(TSVECTOR, nullable=True) # --- Neue Beziehung --- category = relationship("Category", back_populates="posts") # --------------- class Category(Base): __tablename__ = "categories" id = Column(Integer, primary_key=True, index=True) name = Column(String, unique=True, index=True, nullable=False) description = Column(String, nullable=True) posts = relationship("Post", back_populates="category")
Die wichtigsten Änderungen in diesem Schritt sind:
- Erstellung eines neuen 
Category-Modells. - Hinzufügen von 
category_idals Fremdschlüssel imPost-Modell. - Herstellung einer 
relationshipzwischenPostundCategory, die es uns ermöglicht, auf Kategorieinformationen überpost.categoryoder alle Beiträge einer Kategorie übercategory.postszuzugreifen. 
Schritt 2: Datenbanktabellenstruktur aktualisieren
Als nächstes müssen wir diese Tabelle tatsächlich in der Datenbank erstellen und die Tabelle posts ändern.
Erstellen Sie die Tabelle categories
CREATE TABLE categories ( id SERIAL PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL, description TEXT );
Erstellen Sie Standardkategorien
Damit die Kategorien sofort nutzbar sind, erstellen wir manuell zwei:
INSERT INTO categories (name, description) VALUES ('Technical', 'Discuss FastAPI, Python, databases, and other technical topics'), ('General', 'Share daily life, hobbies, etc.');
Modifizieren Sie die Tabelle posts
-- Fügen Sie die Spalte category_id hinzu ALTER TABLE posts ADD COLUMN category_id INTEGER; -- Fügen Sie eine Fremdschlüsselbeschränkung hinzu ALTER TABLE posts ADD CONSTRAINT fk_category FOREIGN KEY(category_id) REFERENCES categories(id);
Bestehende Beiträge behandeln
Für vorhandene Daten in der Tabelle posts müssen wir ihnen eine Standard-category_id zuweisen.
-- Aktualisieren Sie alle vorhandenen Beiträge auf die Kategorie 'General Chat' (angenommen, ihre ID ist 2) UPDATE posts SET category_id = 2 WHERE category_id IS NULL;
Auf Nicht-Null setzen
Schließlich, um die Datenintegrität sicherzustellen, setzen wir die Spalte category_id auf NOT NULL.
ALTER TABLE posts ALTER COLUMN category_id SET NOT NULL;
Wenn Ihre Datenbank mit Leapcell erstellt wurde,
können Sie diese SQL-Anweisungen direkt im webbasierten Bedienfeld ausführen.

Schritt 3: Logik zur Beitragserstellung aktualisieren
Wenn Benutzer jetzt einen Beitrag erstellen, müssen sie eine Kategorie angeben. Wir müssen die Route create_post ändern, um category_id zu akzeptieren.
main.py (Route create_post aktualisieren)
# ... (Vorherige Importe) ... @app.post("/api/posts") async def create_post( title: str = Form(...), content: str = Form(...), category_id: int = Form(...), # category_id hinzugefügt image: Optional[UploadFile] = File(None), db: AsyncSession = Depends(get_db), current_user: Optional[models.User] = Depends(get_current_user) ): # ... (Andere Logik bleibt unverändert) ... # category_id beim Erstellen des Post-Objekts einschließen new_post = models.Post( title=title, content=content, owner_id=current_user.id, image_url=image_url, category_id=category_id # Die Kategorie-ID speichern ) db.add(new_post) await db.commit() await db.refresh(new_post) # 3. Nach dem Posten zur Kategorieseite umleiten, nicht zur Startseite return RedirectResponse(url=f"/categories/{category_id}", status_code=status.HTTP_303_SEE_OTHER)
Schritt 4: Durchsuchen nach Kategorie implementieren
Wir werden /posts nicht mehr als einzigen Seiten-Feed für Beiträge verwenden. Stattdessen erstellen wir eine neue Route GET /categories/{category_id}, um Beiträge aus einer bestimmten Kategorie anzuzeigen.
Wir werden auch die bestehende Route GET /posts ändern, um als Aggregationsseite "Alle Beiträge" zu dienen.
main.py (Routen hinzufügen/ändern)
# ... (Vorherige Importe) ... from sqlalchemy.orm import selectinload # ... (Abhängigkeiten usw.) ... # Hilfsfunktion: Alle Kategorien aus der Datenbank abrufen async def get_all_categories(db: AsyncSession): result = await db.execute(select(models.Category).order_by(models.Category.id)) return result.scalars().all() @app.get("/posts", response_class=HTMLResponse) async def view_posts( request: Request, db: AsyncSession = Depends(get_db), current_user: Optional[models.User] = Depends(get_current_user) ): # Alle Kategorien abfragen (für die Navigation) categories = await get_all_categories(db) # Alle Beiträge abfragen stmt = ( select(models.Post) .options(selectinload(models.Post.owner), selectinload(models.Post.category)) .order_by(desc(models.Post.id)) ) result = await db.execute(stmt) posts = result.scalars().all() return templates.TemplateResponse("posts.html", { "request": request, "posts": posts, "categories": categories, "current_user": current_user, "current_category": None # Markieren, dass wir uns in keiner bestimmten Kategorie befinden }) # --- Neue Route --- @app.get("/categories/{category_id}", response_class=HTMLResponse) async def view_posts_by_category( request: Request, category_id: int, db: AsyncSession = Depends(get_db), current_user: Optional[models.User] = Depends(get_current_user) ): # Alle Kategorien abfragen (für die Navigation) categories = await get_all_categories(db) # Die aktuelle Kategorie abfragen category_result = await db.execute(select(models.Category).where(models.Category.id == category_id)) current_category = category_result.scalar_one_or_none() if not current_category: raise HTTPException(status_code=404, detail="Category not found") # Beiträge in dieser Kategorie abfragen stmt = ( select(models.Post) .where(models.Post.category_id == category_id) .options(selectinload(models.Post.owner), selectinload(models.Post.category)) .order_by(desc(models.Post.id)) ) result = await db.execute(stmt) posts = result.scalars().all() return templates.TemplateResponse("posts.html", { "request": request, "posts": posts, "categories": categories, "current_user": current_user, "current_category": current_category # Aktuelle Kategorieinformationen übergeben }) # ... (Andere Routen) ... @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) ): # Beim Abfragen eines Beitrags auch Kategorieinformationen vorladen result = await db.execute( select(models.Post) .where(models.Post.id == post_id) .options(selectinload(models.Post.owner), selectinload(models.Post.category)) ) post = result.scalar_one_or_none() # ... (Nachfolgende Kommentar-Abfrage-Logik usw. bleibt unverändert) ...
Wir haben eine neue Route GET /categories/{category_id} hinzugefügt, die die Vorlage posts.html wiederverwendet, aber nur die Beiträge für diese Kategorie übergibt. Wir haben auch GET /posts und GET /posts/{post_id} geändert, um sicherzustellen, dass sie Kategorieinformationen korrekt laden und übergeben.
Schritt 5: Frontend-Vorlagen aktualisieren
Schließlich müssen wir die Vorlagen aktualisieren, um die Kategorie-Navigation anzuzeigen, die Auswahl einer Kategorie beim Erstellen von Beiträgen zu ermöglichen und anzuzeigen, zu welcher Kategorie ein Beitrag gehört.
templates/posts.html (Aktualisiert)
<!DOCTYPE html> <html> <head> <style> /* ... (Bestehende Stile) ... */ .category-nav { margin-top: 20px; margin-bottom: 20px; } .category-nav a { margin-right: 15px; text-decoration: none; } .category-nav a.active { font-weight: bold; } .post-category { font-size: 0.9em; color: #888; } </style> </head> <body> <div class="category-nav"> <strong>Categories:</strong> <a href="/posts" class="{{ 'active' if not current_category else '' }}">All</a> {% for category in categories %} <a href="/categories/{{ category.id }}" class="{{ 'active' if current_category and current_category.id == category.id else '' }}"> {{ category.name }} </a> {% endfor %} </div> {% if current_user and not current_user.is_banned %} <h2> Post a new thread {% if current_category %}in {{ current_category.name }} {% endif %} </h2> <form action="/api/posts" method="post" enctype="multipart/form-data"> <input type="text" name="title" placeholder="Post Title" required /><br /> <textarea name="content" rows="4" placeholder="Post Content" required></textarea><br /> <label for="category">Select Category:</label> <select name="category_id" id="category" required> {% for category in categories %} <option value="{{ category.id }}" {{ 'selected' if current_category and current_category.id == category.id else '' }}> {{ category.name }} </option> {% endfor %} </select> <br /><br /> <label for="image">Upload Image (Optional, JPEG/PNG/GIF):</label> <input type="file" name="image" id="image" accept="image/jpeg,image/png,image/gif" /> <br /><br /> <button type="submit">Post</button> </form> {% elif current_user and current_user.is_banned %} {% else %} {% endif %} <hr /> <h2> Post List - {{ current_category.name if current_category else "All Posts" }} </h2> {% for post in posts %} <div class="post-item"> <a href="/posts/{{ post.id }}"><h3>{{ post.title }}</h3></a> <p>{{ post.content }}</p> <small> Author: {{ post.owner.username if post.owner else 'Unknown' }} | Category: <a href="/categories/{{ post.category.id }}">{{ post.category.name }}</a> </small> </div> {% endfor %} </body> </html>
templates/post_detail.html (Aktualisiert)
Auch auf der Detailseite des Beitrags fügen wir die Kategorieinformationen hinzu.
... <body> <div class="post-container"> <h1>{{ post.title }}</h1> <p>{{ post.content }}</p> <small> Author: {{ post.owner.username }} | Category: <a href="/categories/{{ post.category.id }}">{{ post.category.name }}</a> </small> </div> ...
Ausführen und überprüfen
Starten Sie Ihren uvicorn-Server neu:
uvicorn main:app --reload
Besuchen Sie http://127.0.0.1:8000/.
Sie sehen die neue Kategorie-Navigationsleiste oben ("All", "Technical", "General").
Das Formular "Post a new thread" enthält nun eine neue erforderliche Dropdown-Liste "Select Category".

In der Beitragsliste zeigt jeder Beitrag an, zu welcher Kategorie er gehört.

Versuchen Sie, eine Kategorie zu klicken (z. B. "General"). Die Seite wird zu /categories/2 umgeleitet, zeigt nur Beiträge aus dieser Kategorie an, und die Dropdown-Liste "Select Category" im Formular wird standardmäßig auf "General" gesetzt.

Fazit
Durch die Hinzufügung des Category-Modells und die Aktualisierung unserer Routen und Vorlagen haben wir erfolgreich eine Kategoriefunktion für unser Forum implementiert. Es ist nun bequemer für Benutzer, die Inhalte des Forums zu finden und zu durchsuchen.


