Add a "needs review" flag with a sidebar filter
Links can be bookmarked for a later look via a toggle button on the card (🏷️/🔖), shown with a yellow border while flagged. A new sidebar section "Merkliste" links to /?review=1, filtering to just the flagged links; the count updates live via the existing facets query. Since the project has no migration framework, add a minimal startup check that ALTER TABLEs in the needs_review column for existing SQLite databases (create_all only creates missing tables). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
54
app/main.py
54
app/main.py
@@ -6,7 +6,7 @@ from urllib.parse import urlparse
|
||||
from fastapi import Depends, FastAPI, Form, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func, select, text
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
@@ -17,6 +17,16 @@ from .security import hash_password, verify_password
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Einfache Spalten-Migration für bestehende SQLite-Datenbanken (kein Alembic
|
||||
# im Projekt): create_all legt nur neue Tabellen an, keine neuen Spalten.
|
||||
if config.DATABASE_URL.startswith("sqlite"):
|
||||
with engine.begin() as _conn:
|
||||
_cols = {row[1] for row in _conn.execute(text("PRAGMA table_info(links)"))}
|
||||
if "needs_review" not in _cols:
|
||||
_conn.execute(
|
||||
text("ALTER TABLE links ADD COLUMN needs_review BOOLEAN DEFAULT 0")
|
||||
)
|
||||
|
||||
app = FastAPI(title="LinkVault")
|
||||
app.add_middleware(SessionMiddleware, secret_key=config.SECRET_KEY, max_age=60 * 60 * 24 * 14)
|
||||
|
||||
@@ -83,9 +93,15 @@ def facets(db: Session, user: User) -> dict:
|
||||
.group_by(Link.manufacturer)
|
||||
.order_by(func.count().desc())
|
||||
).all()
|
||||
review_count = db.scalar(
|
||||
select(func.count())
|
||||
.select_from(Link)
|
||||
.where(Link.user_id == user.id, Link.needs_review == True) # noqa: E712
|
||||
)
|
||||
return {
|
||||
"categories": [(name, count) for name, count in cat_rows],
|
||||
"manufacturers": [(name, count) for name, count in man_rows],
|
||||
"review_count": review_count or 0,
|
||||
}
|
||||
|
||||
|
||||
@@ -119,12 +135,15 @@ def query_links(
|
||||
manufacturer: str = "",
|
||||
semantic: bool = False,
|
||||
sort: str = DEFAULT_SORT,
|
||||
review_only: bool = False,
|
||||
) -> list[Link]:
|
||||
stmt = select(Link).where(Link.user_id == user.id)
|
||||
if category:
|
||||
stmt = stmt.where(Link.category == category)
|
||||
if manufacturer:
|
||||
stmt = stmt.where(Link.manufacturer == manufacturer)
|
||||
if review_only:
|
||||
stmt = stmt.where(Link.needs_review == True) # noqa: E712
|
||||
links = list(db.scalars(stmt.order_by(Link.created_at.desc())).all())
|
||||
|
||||
q = (q or "").strip()
|
||||
@@ -231,6 +250,7 @@ def index(
|
||||
manufacturer: str = "",
|
||||
semantic: str = "",
|
||||
sort: str = DEFAULT_SORT,
|
||||
review: str = "",
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user = current_user(request, db)
|
||||
@@ -240,7 +260,10 @@ def index(
|
||||
if sort not in SORT_OPTIONS:
|
||||
sort = DEFAULT_SORT
|
||||
is_semantic = semantic in ("1", "on", "true")
|
||||
links = query_links(db, user, q, category, manufacturer, is_semantic, sort)
|
||||
review_only = review in ("1", "on", "true")
|
||||
links = query_links(
|
||||
db, user, q, category, manufacturer, is_semantic, sort, review_only
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{
|
||||
@@ -255,6 +278,7 @@ def index(
|
||||
"ai_enabled": config.AI_ENABLED,
|
||||
"sort": sort,
|
||||
"sort_options": SORT_OPTIONS,
|
||||
"review_only": review_only,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -267,6 +291,7 @@ def search_links(
|
||||
manufacturer: str = "",
|
||||
semantic: str = "",
|
||||
sort: str = DEFAULT_SORT,
|
||||
review: str = "",
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user = current_user(request, db)
|
||||
@@ -276,7 +301,10 @@ def search_links(
|
||||
if sort not in SORT_OPTIONS:
|
||||
sort = DEFAULT_SORT
|
||||
is_semantic = semantic in ("1", "on", "true")
|
||||
links = query_links(db, user, q, category, manufacturer, is_semantic, sort)
|
||||
review_only = review in ("1", "on", "true")
|
||||
links = query_links(
|
||||
db, user, q, category, manufacturer, is_semantic, sort, review_only
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
"_links_list.html", {"request": request, "links": links}
|
||||
)
|
||||
@@ -347,6 +375,26 @@ def delete_link(
|
||||
return Response(status_code=200)
|
||||
|
||||
|
||||
@app.post("/links/{link_id}/toggle-review", response_class=HTMLResponse)
|
||||
def toggle_review(
|
||||
link_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user = current_user(request, db)
|
||||
if not user:
|
||||
return Response(status_code=401, headers={"HX-Redirect": "/login"})
|
||||
link = _get_owned_link(db, user, link_id)
|
||||
if not link:
|
||||
return Response(status_code=404)
|
||||
link.needs_review = not link.needs_review
|
||||
db.commit()
|
||||
db.refresh(link)
|
||||
return templates.TemplateResponse(
|
||||
"_link_card.html", {"request": request, "link": link}
|
||||
)
|
||||
|
||||
|
||||
def _get_owned_link(db: Session, user: User, link_id: int) -> Link | None:
|
||||
link = db.get(Link, link_id)
|
||||
if link and link.user_id == user.id:
|
||||
|
||||
Reference in New Issue
Block a user