DocsUnit 2: data
Build a notes app with Nexy, part 9: paginate the notes
Part 9 shows the notes five at a time, with links to the next and previous pages, read from the
page of the URL: /notes?page=2.
Before you start: the app of part 8.
Ask the database for one page
OFFSET skips the notes of the pages before, LIMIT keeps five. Asking for one more than five
tells, for free, whether a next page exists:
from sqlmodel import Session, select from src.configs.db import enginefrom src.models.note_model import Note def all_notes() -> list[Note]: with Session(engine) as session: return list(session.exec(select(Note).order_by(Note.id))) def find(note_id: int) -> Note | None: with Session(engine) as session: return session.get(Note, note_id) def add_note(text: str) -> Note: note = Note(text=text) with Session(engine) as session: session.add(note) session.commit() session.refresh(note) return note def change_note(note_id: int, text: str) -> bool: with Session(engine) as session: note = session.get(Note, note_id) if note is None: return False note.text = text session.commit() return True def remove_note(note_id: int) -> bool: with Session(engine) as session: note = session.get(Note, note_id) if note is None: return False session.delete(note) session.commit() return True PAGE_SIZE = 5 def notes_page(number: int) -> tuple[list[Note], bool]: """One page of notes, oldest first, and whether a page comes after it.""" with Session(engine) as session: query = select(Note).order_by(Note.id).offset((number - 1) * PAGE_SIZE).limit(PAGE_SIZE + 1) found = list(session.exec(query)) return found[:PAGE_SIZE], len(found) > PAGE_SIZEfrom sqlmodel import Session, select from src.apps.notes.note_model import Notefrom src.configs.db import engine def all_notes() -> list[Note]: with Session(engine) as session: return list(session.exec(select(Note).order_by(Note.id))) def find(note_id: int) -> Note | None: with Session(engine) as session: return session.get(Note, note_id) def add_note(text: str) -> Note: note = Note(text=text) with Session(engine) as session: session.add(note) session.commit() session.refresh(note) return note def change_note(note_id: int, text: str) -> bool: with Session(engine) as session: note = session.get(Note, note_id) if note is None: return False note.text = text session.commit() return True def remove_note(note_id: int) -> bool: with Session(engine) as session: note = session.get(Note, note_id) if note is None: return False session.delete(note) session.commit() return True PAGE_SIZE = 5 def notes_page(number: int) -> tuple[list[Note], bool]: """One page of notes, oldest first, and whether a page comes after it.""" with Session(engine) as session: query = select(Note).order_by(Note.id).offset((number - 1) * PAGE_SIZE).limit(PAGE_SIZE + 1) found = list(session.exec(query)) return found[:PAGE_SIZE], len(found) > PAGE_SIZERead the page number from the URL
The page of the query string is text sent by the visitor: anything that is not a positive number
falls back to the first page, so /notes?page=abc still answers.
from "@/components/NoteCard.nexy" import NoteCardfrom nexy import useSearchParamsfrom src.notes import notes_page params = useSearchParams()error = params.get("error", "")asked = params.get("page", "1")number = max(int(asked), 1) if asked.isdigit() else 1found = notes_page(number)notes = found[0]more = found[1]<section class="app-page"> <h1>Notes</h1> {% if error %} <p class="note-error" role="alert">{{ error | e }}</p> {% endif %} <form method="post" action="{{ __actions_path['create_note'] }}"> <input name="text" placeholder="A new note" maxlength="200" required> <button type="submit">Add</button> </form> {% for note in notes %} <NoteCard id="{{ note.id }}" text="{{ note.text }}" /> {% endfor %} <nav class="note-pages" aria-label="Pages of notes"> {% if number > 1 %}<a href="/notes?page={{ number - 1 }}" rel="prev">Newer</a>{% endif %} {% if more %}<a href="/notes?page={{ number + 1 }}" rel="next">Older</a>{% endif %} </nav></section>from "@/components/NoteCard.nexy" import NoteCardfrom nexy import useSearchParamsfrom src.notes import notes_page params = useSearchParams()error = params.get("error", "")asked = params.get("page", "1")number = max(int(asked), 1) if asked.isdigit() else 1found = notes_page(number)notes = found[0]more = found[1]<section class="app-page"> <h1>Notes</h1> {% if error %} <p class="note-error" role="alert">{{ error | e }}</p> {% endif %} <form method="post" action="{{ __actions_path['create_note'] }}"> <input name="text" placeholder="A new note" maxlength="200" required> <button type="submit">Add</button> </form> {% for note in notes %} <NoteCard id="{{ note.id }}" text="{{ note.text }}" /> {% endfor %} <nav class="note-pages" aria-label="Pages of notes"> {% if number > 1 %}<a href="/notes?page={{ number - 1 }}" rel="prev">Newer</a>{% endif %} {% if more %}<a href="/notes?page={{ number + 1 }}" rel="next">Older</a>{% endif %} </nav></section>rel="prev" and rel="next" tell browsers and search engines how the pages follow each other, and
aria-label names the links for a screen reader.
Add notes until there are more than five: the list stops at five, with an Older link. It leads
to /notes?page=2, which has a Newer link back.
What you learned
offsetandlimitread one page; asking for one row more says whether another page follows.useSearchParams()reads?page=; a value from the visitor is checked before it is used.rel="prev"andrel="next"link the pages for browsers, crawlers and screen readers.
Next steps
- Unit 3, part 10: create an account and sign in
- SQLite: indexes, pragmas and tests on an in-memory database