Fix shared decks route ordering, fix corrupted middot character

- Move GET /shared before GET /{deck_id} so FastAPI matches it correctly
  (was being caught by /{deck_id} and failing int parse on "shared")
- Remove duplicate /shared endpoint left after move
- Fix corrupted middot character (U+FFFD) in My Decks card count display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-04-04 23:45:13 +02:00
parent 18d0a74348
commit be7efd73d2
2 changed files with 62 additions and 68 deletions

View file

@ -147,6 +147,67 @@ def list_trashed_decks(
).order_by(FlashcardDeck.deleted_at.desc()).all()
@router.get("/shared")
def list_shared_decks(
limit: int = Query(20, le=100),
offset: int = Query(0),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""List shared decks sorted by average rating (desc), with pagination."""
from sqlalchemy import func
avg_rating = db.query(
FlashcardDeckRating.deck_id,
func.avg(FlashcardDeckRating.rating).label("avg_rating"),
func.count(FlashcardDeckRating.id).label("rating_count"),
).group_by(FlashcardDeckRating.deck_id).subquery()
my_ratings = {}
for r in db.query(FlashcardDeckRating).filter(FlashcardDeckRating.user_id == current_user.id).all():
my_ratings[r.deck_id] = r.rating
total = db.query(FlashcardDeck).filter(
FlashcardDeck.is_shared == 1,
FlashcardDeck.deleted_at.is_(None),
).count()
decks_with_rating = db.query(
FlashcardDeck,
func.coalesce(avg_rating.c.avg_rating, 0).label("avg_rating"),
func.coalesce(avg_rating.c.rating_count, 0).label("rating_count"),
).outerjoin(
avg_rating, FlashcardDeck.id == avg_rating.c.deck_id
).filter(
FlashcardDeck.is_shared == 1,
FlashcardDeck.deleted_at.is_(None),
).order_by(
func.coalesce(avg_rating.c.avg_rating, 0).desc(),
FlashcardDeck.created_at.desc(),
).offset(offset).limit(limit).all()
user_cache: dict[int, str] = {}
result = []
for deck, avg_r, r_count in decks_with_rating:
if deck.user_id not in user_cache:
owner = db.query(User).filter(User.id == deck.user_id).first()
user_cache[deck.user_id] = owner.name if owner else "Unknown"
result.append({
"id": deck.id,
"title": deck.title,
"card_count": deck.card_count,
"user_id": deck.user_id,
"owner_name": user_cache[deck.user_id],
"is_shared": deck.is_shared,
"created_at": deck.created_at,
"avg_rating": round(float(avg_r), 1),
"rating_count": int(r_count),
"my_rating": my_ratings.get(deck.id),
})
return {"total": total, "decks": result}
@router.get("/{deck_id}", response_model=FlashcardDeckDetail)
def get_flashcard_deck(
deck_id: int,
@ -224,73 +285,6 @@ def admin_unshare_deck(
db.commit()
@router.get("/shared")
def list_shared_decks(
limit: int = Query(20, le=100),
offset: int = Query(0),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""List shared decks sorted by average rating (desc), with pagination."""
from sqlalchemy import func
# Subquery for average rating
avg_rating = db.query(
FlashcardDeckRating.deck_id,
func.avg(FlashcardDeckRating.rating).label("avg_rating"),
func.count(FlashcardDeckRating.id).label("rating_count"),
).group_by(FlashcardDeckRating.deck_id).subquery()
# Current user's rating per deck
my_ratings = {}
if current_user:
for r in db.query(FlashcardDeckRating).filter(FlashcardDeckRating.user_id == current_user.id).all():
my_ratings[r.deck_id] = r.rating
query = db.query(FlashcardDeck).filter(
FlashcardDeck.is_shared == 1,
FlashcardDeck.deleted_at.is_(None),
)
total = query.count()
# Join with avg rating for sorting
decks_with_rating = db.query(
FlashcardDeck,
func.coalesce(avg_rating.c.avg_rating, 0).label("avg_rating"),
func.coalesce(avg_rating.c.rating_count, 0).label("rating_count"),
).outerjoin(
avg_rating, FlashcardDeck.id == avg_rating.c.deck_id
).filter(
FlashcardDeck.is_shared == 1,
FlashcardDeck.deleted_at.is_(None),
).order_by(
func.coalesce(avg_rating.c.avg_rating, 0).desc(),
FlashcardDeck.created_at.desc(),
).offset(offset).limit(limit).all()
# Get owner names
user_cache: dict[int, str] = {}
result = []
for deck, avg_r, r_count in decks_with_rating:
if deck.user_id not in user_cache:
owner = db.query(User).filter(User.id == deck.user_id).first()
user_cache[deck.user_id] = owner.name if owner else "Unknown"
result.append({
"id": deck.id,
"title": deck.title,
"card_count": deck.card_count,
"user_id": deck.user_id,
"owner_name": user_cache[deck.user_id],
"is_shared": deck.is_shared,
"created_at": deck.created_at,
"avg_rating": round(float(avg_r), 1),
"rating_count": int(r_count),
"my_rating": my_ratings.get(deck.id),
})
return {"total": total, "decks": result}
@router.post("/{deck_id}/rate")
def rate_deck(
deck_id: int,

View file

@ -191,7 +191,7 @@ export default function FlashcardsPage() {
<div key={deck.id} className="card" style={{ padding: 20 }}>
<h3 style={{ fontSize: '1rem', marginBottom: 6 }}>{deck.title}</h3>
<p style={{ color: 'var(--text-muted)', fontSize: '0.85rem', marginBottom: 4 }}>
{deck.card_count} cards <EFBFBD><EFBFBD> {new Date(deck.created_at).toLocaleDateString()}
{deck.card_count} cards &middot; {new Date(deck.created_at).toLocaleDateString()}
</p>
{deck.is_shared ? (
<span style={{ fontSize: '0.72rem', padding: '2px 7px', borderRadius: 10, background: '#dbeafe', color: '#1d4ed8', fontWeight: 600 }}>Shared</span>