Favorites Feature and Admin Management

In this chapter, you will learn how to implement a favorites feature using a many-to-many relationship, and use sqladmin to set up an admin backend.


Many-to-Many Relationship Design

Example

# File path: models.py - add association table and modify Post model
from sqlalchemy import Table

# Association table: many-to-many intermediate table between User and Post
favorites = Table(
    "favorites",
    Base.metadata,
    Column("user_id", Integer, ForeignKey("users.id"), primary_key=True),
    Column("post_id", Integer, ForeignKey("posts.id"), primary_key=True),
    Column("created_at", DateTime, default=datetime.utcnow)
)

Execute migration:

(venv) $ alembic revision --autogenerate -m "新增收藏关联表"
(venv) $ alembic upgrade head

Favorite and Unfavorite Routes

Example

# File path: routers/favorites.py
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from database import get_db
from models import Post, favorites
from routers.users import get_current_user
from fastapi.templating import Jinja2Templates

router = APIRouter(prefix="/favorites", tags=["Favorites"])
templates = Jinja2Templates(directory="templates")

@router.post("/toggle/{post_id}")
def toggle_favorite(
    post_id: int,
    request: Request,
    db: Session = Depends(get_db),
    current_user = Depends(get_current_user)    # Login Required
):
    """Toggle favorite status"""
    post = db.query(Post).filter(Post.id == post_id).first()
    if not post:
        raise HTTPException(status_code=404, detail=“Article does not exist”)

    # Check if already favorited
    is_faved = db.query(favorites).filter(
        favorites.c.user_id == current_user.id,
        favorites.c.post_id == post.id
    ).first() is not None

    if is_faved:
        # Unfavorite
        db.execute(
            favorites.delete().where(
                (favorites.c.user_id == current_user.id) &
                (favorites.c.post_id == post.id)
            )
        )
        db.commit()
    else:
        # Add favorite
        db.execute(
            favorites.insert().values(user_id=current_user.id, post_id=post.id)
        )
        db.commit()

    from fastapi.responses import RedirectResponse
    return RedirectResponse(url=request.headers.get("referer", "/"), status_code=303)


@router.get("/", name="favorites_list")
def favorites_list(
    request: Request,
    db: Session = Depends(get_db),
    current_user = Depends(get_current_user)
):
    """My favorites list"""
    faved_posts = db.query(Post).join(
        favorites, (favorites.c.post_id == Post.id)
    ).filter(
        favorites.c.user_id == current_user.id
    ).order_by(favorites.c.created_at.desc()).all()

    return templates.TemplateResponse("favorites.html", {
        "request": request,
        "posts": faved_posts
    })

Add a Favorite Button on the Detail Page

Example

Initial parameters have been set.
<div class="post-header">
    <h1>{{ post.title }}</h1>
    <form method="post" action="/favorites/toggle/{{ post.id }}">
        <button type="submit" class="fav-btn">♡ Favorite</button>
    </form>
</div>

sqladmin — Admin Management

sqladminIt is the admin backend extension in the FastAPI ecosystem that parallels Django Admin.

It is designed specifically for SQLAlchemy and integrates seamlessly with FastAPI.

(venv) $ pip install sqladmin

Example

# File path: configure sqladmin in main.py
from sqladmin import Admin, ModelView
from database import engine
from models import Post, Category, User

class PostAdmin(ModelView, model=Post):
    column_list = [Post.id, Post.title, Post.category, Post.created_at]
    column_searchable_list = [Post.title, Post.summary]
    column_sortable_list = [Post.created_at, Post.title]
    form_columns = [Post.title, Post.slug, Post.summary, Post.content, Post.category_id]

class CategoryAdmin(ModelView, model=Category):
    column_list = [Category.id, Category.name, Category.slug]
    form_columns = [Category.name, Category.slug]

class UserAdmin(ModelView, model=User):
    column_list = [User.id, User.username, User.email]
    column_searchable_list = [User.username, User.email]
    # Do not display the password field (for security)
    form_excluded_columns = [User.hashed_password]

# Create an Admin instance and bind it to the FastAPI app
admin = Admin(app, engine, title=EXAMPLE Blog Admin)
admin.add_view(PostAdmin)
admin.add_view(CategoryAdmin)
admin.add_view(UserAdmin)

Visit /admin to enter the admin interface.

sqladmin vs Flask-Admin vs Django Admin

FeaturesDjango AdminFlask-Adminsqladmin
Registration methodadmin.site.register()admin.add_view(ModelView())Admin + add_view
ORM SupportDjango ORMSQLAlchemy / MongoEngineSQLAlchemy (only)
Authentication supportBuilt-inRequires manual integrationBuilt-in authentication_backend
Async supportNot supportedNot supportednative async

sqladmin is accessible without login by default. It needs to be configured for production environments.authentication_backendTo add authentication protection, see the official sqladmin documentation.


Chapter summary

In this chapter, you implemented two important features: using a Table association table to implement the many-to-many relationship for favorites; and using sqladmin to quickly set up an admin backend management system.

The blog now has complete user interaction features (registration, login, favorites) and management functions (Admin backend).

other extensions