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:
parent
18d0a74348
commit
be7efd73d2
2 changed files with 62 additions and 68 deletions
|
|
@ -147,6 +147,67 @@ def list_trashed_decks(
|
||||||
).order_by(FlashcardDeck.deleted_at.desc()).all()
|
).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)
|
@router.get("/{deck_id}", response_model=FlashcardDeckDetail)
|
||||||
def get_flashcard_deck(
|
def get_flashcard_deck(
|
||||||
deck_id: int,
|
deck_id: int,
|
||||||
|
|
@ -224,73 +285,6 @@ def admin_unshare_deck(
|
||||||
db.commit()
|
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")
|
@router.post("/{deck_id}/rate")
|
||||||
def rate_deck(
|
def rate_deck(
|
||||||
deck_id: int,
|
deck_id: int,
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ export default function FlashcardsPage() {
|
||||||
<div key={deck.id} className="card" style={{ padding: 20 }}>
|
<div key={deck.id} className="card" style={{ padding: 20 }}>
|
||||||
<h3 style={{ fontSize: '1rem', marginBottom: 6 }}>{deck.title}</h3>
|
<h3 style={{ fontSize: '1rem', marginBottom: 6 }}>{deck.title}</h3>
|
||||||
<p style={{ color: 'var(--text-muted)', fontSize: '0.85rem', marginBottom: 4 }}>
|
<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 · {new Date(deck.created_at).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
{deck.is_shared ? (
|
{deck.is_shared ? (
|
||||||
<span style={{ fontSize: '0.72rem', padding: '2px 7px', borderRadius: 10, background: '#dbeafe', color: '#1d4ed8', fontWeight: 600 }}>Shared</span>
|
<span style={{ fontSize: '0.72rem', padding: '2px 7px', borderRadius: 10, background: '#dbeafe', color: '#1d4ed8', fontWeight: 600 }}>Shared</span>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue