feat: replace per-author cap with interleaving to avoid dropping posts in feed and home
The old `diversify_by_author` capped each author to 2 posts per page, silently dropping excess rows. The new `interleave_by_author` reorders the full result set so no two consecutive posts share an author, preserving every post while breaking up same-author runs. `paginate_diverse` loses its `max_per_author` and `pool` parameters; the home route now fetches exactly 6 rows and interleaves them instead of over-fetching 50 and capping. Tests are rewritten to verify interleaving behavior and per-author chronological order preservation.
This commit is contained in:
@@ -78,7 +78,7 @@ Tests run on port 10501 with a tempfile SQLite DB and a dedicated `DEVPLACE_DATA
|
||||
|
||||
`@app.on_event("startup")` calls `init_db()` and, unless `DEVPLACE_DISABLE_SERVICES` is set, registers `NewsService` with `service_manager` and kicks off `start_all()` as an asyncio task. `@app.on_event("shutdown")` calls `service_manager.stop_all()`.
|
||||
|
||||
`GET /` is the home page and never redirects: guests get the marketing splash, authenticated users get a personalized home (welcome card with their avatar/stats, a feed shortcut, quick links), both sharing the Latest Posts + Developer News sections. It pulls up to 6 articles where `show_on_landing=1` from the `news` table plus 6 recent posts (joined via the batch helpers in `database.py`). The Latest Posts section and the main `/feed` enforce **author diversity** (at most two posts per author) via `diversify_by_author` / `paginate_diverse` in `database.py` - `paginate_diverse` mirrors `paginate` (same `(rows, next_cursor)` contract) and enforces the cap per page.
|
||||
`GET /` is the home page and never redirects: guests get the marketing splash, authenticated users get a personalized home (welcome card with their avatar/stats, a feed shortcut, quick links), both sharing the Latest Posts + Developer News sections. It pulls up to 6 articles where `show_on_landing=1` from the `news` table plus 6 recent posts (joined via the batch helpers in `database.py`). The Latest Posts section and the main `/feed` enforce **author diversity** by *interleaving* authors (never dropping posts) via `interleave_by_author` / `paginate_diverse` in `database.py` - the page set is the chronological window, then reordered so no two consecutive posts share an author while each author's own posts stay in date order (a run from one author is broken up by deferring to the next available author, recursively; an all-one-author page stays as-is). `paginate_diverse` mirrors `paginate` (same `(rows, next_cursor)` contract): the interleave is a pure permutation of the page, so the cursor (oldest `created_at` in the page) is unchanged.
|
||||
|
||||
### Routing layout
|
||||
Routers in `devplacepy/routers/` are organised as a **directory tree that mirrors the endpoint (URL) path**, exactly like `tests/`. A domain with a single resource stays one flat file (`feed.py`, `posts.py`, `gists.py`, ...); a domain with several sub-resources is a **package directory** split **one file per sub-resource** (a distinct noun under the domain), never one file per individual endpoint. Each leaf module declares its own `router = APIRouter()` keeping the exact path strings, and the package `__init__.py` aggregates them with `router.include_router(...)`. Because a package exposes `.router` like a module, `main.py` mounts each domain at its prefix unchanged. **Aggregation rules:** a leaf that owns the domain's collection-root (`""`) route must be the package's base router (FastAPI rejects an empty path included under an empty prefix), so the package `__init__` imports that leaf's `router` and includes the rest onto it (see `routers/issues`, `routers/admin`, `routers/projects`); leaves carved out of a former monolith keep their relative path strings and are included with no sub-prefix, while a router folded in from a deeper mount keeps its own paths and is included with a sub-prefix (`admin/__init__` includes `services.router` with `prefix="/services"`). Module-private helpers shared across a package's leaves live in its `_shared.py`. Prefixes are wired in `main.py`:
|
||||
|
||||
Reference in New Issue
Block a user