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:
@@ -81,13 +81,16 @@ def get_api_user_optional(
|
||||
|
||||
class ProfileCreate(BaseModel):
|
||||
nickname: str
|
||||
country: str | None = None
|
||||
|
||||
class ProfileUpdate(BaseModel):
|
||||
nickname: str | None = None
|
||||
country: str | None = None
|
||||
|
||||
class ProfileOut(BaseModel):
|
||||
id: int
|
||||
nickname: str
|
||||
country: str | None
|
||||
showcase_enabled: bool
|
||||
showcase_visible: str
|
||||
notes: str
|
||||
@@ -101,6 +104,7 @@ class PostcardCreate(BaseModel):
|
||||
status: str
|
||||
recipient_name: str | None = None
|
||||
country: str | None = None
|
||||
country_from: str | None = None
|
||||
send_time: str | None = None
|
||||
arrival_time: str | None = None
|
||||
sender_name: str | None = None
|
||||
@@ -112,6 +116,7 @@ class PostcardUpdate(BaseModel):
|
||||
status: str | None = None
|
||||
recipient_name: str | None = None
|
||||
country: str | None = None
|
||||
country_from: str | None = None
|
||||
send_time: str | None = None
|
||||
arrival_time: str | None = None
|
||||
sender_name: str | None = None
|
||||
@@ -125,6 +130,7 @@ class PostcardOut(BaseModel):
|
||||
status: str
|
||||
recipient_name: str | None
|
||||
country: str | None
|
||||
country_from: str | None
|
||||
send_time: datetime | None
|
||||
arrival_time: datetime | None
|
||||
sender_name: str | None
|
||||
@@ -205,7 +211,8 @@ def create_profile(
|
||||
db: DbSession = Depends(get_db),
|
||||
ak: ApiKey = Depends(require_scopes(PERM_PROFILES_WRITE)),
|
||||
):
|
||||
p = Profile(nickname=body.nickname.strip(), user_id=ak.user_id)
|
||||
c = body.country.strip().upper() if body.country else None
|
||||
p = Profile(nickname=body.nickname.strip(), user_id=ak.user_id, country=c)
|
||||
db.add(p)
|
||||
db.commit()
|
||||
db.refresh(p)
|
||||
@@ -231,6 +238,8 @@ def update_profile(
|
||||
p = _profile_or_404(ak.user, profile_id, db)
|
||||
if body.nickname is not None:
|
||||
p.nickname = body.nickname.strip()
|
||||
if body.country is not None:
|
||||
p.country = body.country.strip().upper() if body.country else None
|
||||
db.commit()
|
||||
db.refresh(p)
|
||||
return p
|
||||
@@ -275,7 +284,10 @@ def create_postcard(
|
||||
):
|
||||
_profile_or_404(ak.user, profile_id, db)
|
||||
cn = body.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 == ak.user.id
|
||||
).first()
|
||||
if dup:
|
||||
raise HTTPException(status_code=409, detail="Card number already exists")
|
||||
pc = Postcard(
|
||||
profile_id=profile_id,
|
||||
@@ -283,6 +295,7 @@ def create_postcard(
|
||||
status=body.status,
|
||||
recipient_name=body.recipient_name,
|
||||
country=body.country.strip().upper() if body.country else None,
|
||||
country_from=body.country_from.strip().upper() if body.country_from else None,
|
||||
send_time=_parse_date(body.send_time),
|
||||
arrival_time=_parse_date(body.arrival_time),
|
||||
sender_name=body.sender_name,
|
||||
@@ -320,12 +333,17 @@ def update_postcard(
|
||||
setattr(pc, field, val.strip() if isinstance(val, str) else val)
|
||||
if body.card_number is not None:
|
||||
cn = body.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 == ak.user.id
|
||||
).first()
|
||||
if dup:
|
||||
raise HTTPException(status_code=409, detail="Card number already exists")
|
||||
pc.card_number = cn
|
||||
if body.country is not None:
|
||||
pc.country = body.country.strip().upper() if body.country else None
|
||||
if body.country_from is not None:
|
||||
pc.country_from = body.country_from.strip().upper() if body.country_from else None
|
||||
for field in ("send_time", "arrival_time", "receive_time"):
|
||||
val = getattr(body, field)
|
||||
if val is not None:
|
||||
|
||||
@@ -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