feat(postcard): profile country, country_from, per-user card number uniqueness, public page overhaul - Profile: add country field for default origin country - Postcard: add country_from field, auto-fill from profile.country on create - card_number uniqueness: change from global to per-user (profile_id + card_number) - Public pages: remove card_number display, only show cards with image_front - Public pages: show country as from→to format - Index page: no time display, add register button, fix lightbox with prev/next - Infinite scroll: add /api/public/cards endpoints and IntersectionObserver - Internal pages: update postcards list/detail for country_from display - i18n: add profiles.country_ph, pc_form.country_from, pc_detail.route
This commit is contained in:
@@ -4,7 +4,7 @@ from random import shuffle
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, Query, Request, UploadFile
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -126,7 +126,8 @@ def index_page(request: Request, db: Session = Depends(get_db)):
|
||||
).all()
|
||||
for p in profiles:
|
||||
cards = db.query(Postcard).filter(
|
||||
Postcard.profile_id == p.id, Postcard.showcase_hidden == False
|
||||
Postcard.profile_id == p.id, Postcard.showcase_hidden == False,
|
||||
Postcard.image_front.isnot(None), Postcard.image_front != ""
|
||||
).all()
|
||||
for c in cards:
|
||||
visible = True
|
||||
@@ -147,7 +148,92 @@ def index_page(request: Request, db: Session = Depends(get_db)):
|
||||
lang = "zh"
|
||||
|
||||
return templates.TemplateResponse("index.html", {
|
||||
"request": request, "postcards": all_postcards, "lang": lang,
|
||||
"request": request, "postcards": all_postcards[:20], "lang": lang,
|
||||
"has_more": len(all_postcards) > 20,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/api/public/cards")
|
||||
def api_public_cards(
|
||||
request: Request,
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
all_cards: list[Postcard] = []
|
||||
users = db.query(User).filter(User.showcase_mode.isnot(None)).all()
|
||||
for u in users:
|
||||
profiles = db.query(Profile).filter(
|
||||
Profile.user_id == u.id, Profile.showcase_enabled == True
|
||||
).all()
|
||||
for p in profiles:
|
||||
cards = db.query(Postcard).filter(
|
||||
Postcard.profile_id == p.id, Postcard.showcase_hidden == False,
|
||||
Postcard.image_front.isnot(None), Postcard.image_front != ""
|
||||
).all()
|
||||
for c in cards:
|
||||
vis = True
|
||||
if c.status in ("sent", "delivered") and p.showcase_visible not in ("sent", "both"):
|
||||
vis = False
|
||||
if c.status == "received" and p.showcase_visible not in ("received", "both"):
|
||||
vis = False
|
||||
if vis:
|
||||
all_cards.append(c)
|
||||
shuffle(all_cards)
|
||||
total = len(all_cards)
|
||||
start = (page - 1) * limit
|
||||
batch = all_cards[start:start + limit]
|
||||
return JSONResponse({
|
||||
"total": total,
|
||||
"page": page,
|
||||
"has_more": start + limit < total,
|
||||
"cards": [{
|
||||
"image_front": c.image_front,
|
||||
"country_from": c.country_from,
|
||||
"country": c.country,
|
||||
"status": c.status,
|
||||
} for c in batch],
|
||||
})
|
||||
|
||||
|
||||
@router.get("/api/public/cards/{username}")
|
||||
def api_public_cards_user(
|
||||
username: str,
|
||||
request: Request,
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
tab: str = Query("sent"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if not user:
|
||||
return JSONResponse({"error": "not found"}, status_code=404)
|
||||
profiles = db.query(Profile).filter(
|
||||
Profile.user_id == user.id, Profile.showcase_enabled == True
|
||||
).all()
|
||||
all_cards: list[Postcard] = []
|
||||
for p in profiles:
|
||||
cards = db.query(Postcard).filter(
|
||||
Postcard.profile_id == p.id, Postcard.showcase_hidden == False,
|
||||
Postcard.image_front.isnot(None), Postcard.image_front != ""
|
||||
).all()
|
||||
if tab == "sent" and p.showcase_visible in ("sent", "both"):
|
||||
all_cards.extend([c for c in cards if c.status in ("sent", "delivered")])
|
||||
elif tab == "received" and p.showcase_visible in ("received", "both"):
|
||||
all_cards.extend([c for c in cards if c.status == "received"])
|
||||
all_cards.sort(key=lambda c: c.send_time or c.created_at, reverse=True)
|
||||
total = len(all_cards)
|
||||
start = (page - 1) * limit
|
||||
batch = all_cards[start:start + limit]
|
||||
return JSONResponse({
|
||||
"total": total,
|
||||
"page": page,
|
||||
"has_more": start + limit < total,
|
||||
"cards": [{
|
||||
"image_front": c.image_front,
|
||||
"country_from": c.country_from,
|
||||
"country": c.country,
|
||||
} for c in batch],
|
||||
})
|
||||
|
||||
|
||||
@@ -565,8 +651,9 @@ def profile_list(request: Request, user: User = Depends(get_current_user_web), d
|
||||
|
||||
|
||||
@router.post("/profiles")
|
||||
def profile_create(request: Request, nickname: str = Form(...), user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
||||
db.add(Profile(user_id=user.id, nickname=nickname))
|
||||
def profile_create(request: Request, nickname: str = Form(...), country: str = Form(""), user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
||||
c = country.strip().upper() if country else None
|
||||
db.add(Profile(user_id=user.id, nickname=nickname, country=c))
|
||||
db.commit()
|
||||
return _redirect("/profiles")
|
||||
|
||||
@@ -581,10 +668,11 @@ def profile_delete(profile_id: int, user: User = Depends(get_current_user_web),
|
||||
|
||||
|
||||
@router.post("/profiles/{profile_id}/rename")
|
||||
def profile_rename(profile_id: int, nickname: str = Form(...), user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
||||
def profile_rename(profile_id: int, nickname: str = Form(...), country: str = Form(""), user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
||||
p = db.query(Profile).filter(Profile.id == profile_id, Profile.user_id == user.id).first()
|
||||
if p:
|
||||
p.nickname = nickname
|
||||
p.country = country.strip().upper() if country else None
|
||||
db.commit()
|
||||
return _redirect("/profiles")
|
||||
|
||||
@@ -649,6 +737,7 @@ def postcard_create(
|
||||
status: str = Form(...),
|
||||
recipient_name: str = Form(""),
|
||||
country: str = Form(""),
|
||||
country_from: str = Form(""),
|
||||
send_time: str = Form(""),
|
||||
arrival_time: str = Form(""),
|
||||
sender_name: str = Form(""),
|
||||
@@ -662,7 +751,10 @@ def postcard_create(
|
||||
return _redirect("/profiles")
|
||||
|
||||
cn = card_number.strip()
|
||||
if db.query(Postcard).filter(Postcard.card_number == cn).first():
|
||||
dup = db.query(Postcard).join(Profile).filter(
|
||||
Postcard.card_number == cn, Profile.user_id == user.id
|
||||
).first()
|
||||
if dup:
|
||||
return templates.TemplateResponse("postcard_form.html", {
|
||||
"request": request, "user": user, "profile": profile, "postcard": None,
|
||||
"error": _t("pc_list.duplicate", user.language),
|
||||
@@ -686,6 +778,7 @@ def postcard_create(
|
||||
status=status,
|
||||
recipient_name=recipient_name or None,
|
||||
country=(country.strip().upper() if country else None),
|
||||
country_from=(country_from.strip().upper() if country_from else profile.country),
|
||||
send_time=_parse_dt(send_time),
|
||||
arrival_time=_parse_dt(arrival_time),
|
||||
sender_name=sender_name or None,
|
||||
@@ -731,6 +824,7 @@ def postcard_update(
|
||||
status: str = Form(...),
|
||||
recipient_name: str = Form(""),
|
||||
country: str = Form(""),
|
||||
country_from: str = Form(""),
|
||||
send_time: str = Form(""),
|
||||
arrival_time: str = Form(""),
|
||||
sender_name: str = Form(""),
|
||||
@@ -749,7 +843,10 @@ def postcard_update(
|
||||
return _redirect("/profiles")
|
||||
|
||||
cn = card_number.strip()
|
||||
dup = db.query(Postcard).filter(Postcard.card_number == cn, Postcard.id != postcard_id).first()
|
||||
dup = db.query(Postcard).join(Profile).filter(
|
||||
Postcard.card_number == cn, Postcard.id != postcard_id,
|
||||
Profile.user_id == user.id
|
||||
).first()
|
||||
if dup:
|
||||
return templates.TemplateResponse("postcard_form.html", {
|
||||
"request": request, "user": user, "profile": pc.profile, "postcard": pc,
|
||||
@@ -772,6 +869,7 @@ def postcard_update(
|
||||
pc.status = status
|
||||
pc.recipient_name = recipient_name or None
|
||||
pc.country = country or None
|
||||
pc.country_from = country_from.strip().upper() if country_from else pc.country_from
|
||||
pc.send_time = _parse_dt(send_time)
|
||||
pc.arrival_time = None if status == "sent" else _parse_dt(arrival_time)
|
||||
pc.sender_name = sender_name or None
|
||||
@@ -925,7 +1023,10 @@ def public_showcase(request: Request, username: str, db: Session = Depends(get_d
|
||||
profiles = db.query(Profile).filter(Profile.user_id == user.id, Profile.showcase_enabled == True).all()
|
||||
sent_all, received_all, profile_groups = [], [], []
|
||||
for p in profiles:
|
||||
cards = db.query(Postcard).filter(Postcard.profile_id == p.id, Postcard.showcase_hidden == False).all()
|
||||
cards = db.query(Postcard).filter(
|
||||
Postcard.profile_id == p.id, Postcard.showcase_hidden == False,
|
||||
Postcard.image_front.isnot(None), Postcard.image_front != ""
|
||||
).all()
|
||||
if p.showcase_visible in ("sent", "both"):
|
||||
sent_all.extend([c for c in cards if c.status in ("sent", "delivered")])
|
||||
if p.showcase_visible in ("received", "both"):
|
||||
|
||||
Reference in New Issue
Block a user