From 6fdc5f4dd1a022350ff1ca81717765afd3c0d6d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 03:15:01 +0000 Subject: [PATCH] library: simple home page (look up, check out, return, add, add person, recently added), hamburger menu, footer; add books by title with matches to choose Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf --- library/README.md | 13 +- .../quickadd/__pycache__/app.cpython-313.pyc | Bin 127834 -> 136607 bytes library/quickadd/app.py | 101 ++++++++++ library/quickadd/index.html | 186 +++++++++++++++--- 4 files changed, 270 insertions(+), 30 deletions(-) diff --git a/library/README.md b/library/README.md index b002d3f..c94bded 100644 --- a/library/README.md +++ b/library/README.md @@ -208,7 +208,18 @@ quickadd/ the scan-to-add service (Python standard library + yaz) Staff who are signed in to the library site (and allowed to edit the catalogue) open **Add books** without a second login. Koha itself confirms the sign-in; the shared secret is `SSO_SECRET` in `.env`. The read-only `guest` login still uses the Add books prompt, and the direct Add books port never accepts a Koha sign-in. If `SSO_SECRET` is empty, Add books just asks for its own login as before. -## Everyday use (the Library site at `/add/`) +## The home page (the Room for Books site at `/add/`) + +One simple page: a **Look up** box (books by title, author or ISBN, and people by name or card number), four big buttons +(**Check out**, **Return**, **Add books**, **Add a person**) and **Recently added** books with their covers. The **menu** +(top right) holds the rest: browse all books, lend / return, people and library cards, barcodes and settings, export / import, +and a link to Koha's own staff screens. Borrowers and the read-only guest see just the lookup, recent books and (borrowers) My books. + +**Adding by title:** on Add books, scan or type an ISBN to add it straight away; type a title or author instead and you get a list +of matches (cover, author, year, publisher) from Open Library and the Library of Congress to choose from. Books you already have +are marked, with an *Add another copy* button. + +## Everyday use (details) One page for the day-to-day, so you rarely need Koha's own screens: diff --git a/library/quickadd/__pycache__/app.cpython-313.pyc b/library/quickadd/__pycache__/app.cpython-313.pyc index 3d80282702d8ce02b214260de31d91435772a284..687801fa94b64934f00435b9d2c77336355f34fc 100644 GIT binary patch delta 25379 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 delta 18451 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 LJjWZkT^ar_Y?Ha} diff --git a/library/quickadd/app.py b/library/quickadd/app.py index 7e3cfcc..8694ab7 100644 --- a/library/quickadd/app.py +++ b/library/quickadd/app.py @@ -1527,6 +1527,101 @@ def op_logins_all(payload): return 200, {"status": "done", "made": made} +# ------------------------------------------------------------ title / author search +def z3950_candidates(spec, term, limit=8): + """Title search (falling back to any field) on a Z39.50 server; returns summaries with an ISBN.""" + out = [] + for attr in ("@attr 1=4", "@attr 1=1016"): + with tempfile.TemporaryDirectory() as tmp: + dump = os.path.join(tmp, "rec.mrc") + q = term.replace('"', " ") + script = 'format usmarc\nfind %s "%s"\nshow 1+%d\nquit\n' % (attr, q, limit) + try: + res = subprocess.run(["yaz-client", "-m", dump, spec], input=script, capture_output=True, text=True, + timeout=Z3950_TIMEOUT).stdout + except (subprocess.TimeoutExpired, OSError): + return out + m = re.search(r"Number of hits: (\d+)", res) + if not m or int(m.group(1)) == 0 or not os.path.exists(dump): + continue + raw = open(dump, "rb").read() + args = ["yaz-marcdump", "-o", "marcxml"] + if len(raw) > 9 and raw[9:10] == b" ": + args += ["-f", "MARC-8", "-t", "UTF-8", "-l", "9=97"] + conv = subprocess.run(args + [dump], capture_output=True, text=True, timeout=15) + if conv.returncode != 0: + continue + try: + root = ET.fromstring(conv.stdout if conv.stdout.lstrip().startswith("%s" % (NS["m"], conv.stdout)) + except ET.ParseError: + continue + for rec in root.iter("{%s}record" % NS["m"]): + info = describe(rec) + isbn = "" + for df in rec.findall("m:datafield[@tag='020']", NS): + for sf in df.findall("m:subfield[@code='a']", NS): + c = clean_code((sf.text or "").split(" ")[0]) + if c and len(c) in (10, 13) and not isbn: + isbn = isbn10_to_13(c) if len(c) == 10 else c + if isbn: + out.append(dict(info, isbn=isbn, source="Library of Congress")) + if out: + return out + return out + + +def openlibrary_candidates(term, limit=10): + url = "https://openlibrary.org/search.json?" + urllib.parse.urlencode( + {"q": term, "limit": str(limit), "fields": "title,author_name,first_publish_year,isbn,publisher,cover_i"}) + try: + with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "library-quickadd/1.0"}), timeout=10) as r: + docs = json.load(r).get("docs") or [] + except (urllib.error.URLError, OSError, ValueError): + return [] + out = [] + for d in docs: + isbns = [clean_code(i) for i in (d.get("isbn") or [])] + isbns = [i for i in isbns if i and len(i) == 13 and i[:3] in ("978", "979")] or [isbn10_to_13(i) for i in isbns if i and len(i) == 10] + if not isbns or not d.get("title"): + continue + out.append({"title": d["title"], "author": ", ".join((d.get("author_name") or [])[:2]), "year": str(d.get("first_publish_year") or ""), + "publisher": ((d.get("publisher") or [""])[0])[:60], "isbn": isbns[0], "source": "Open Library"}) + return out + + +def op_lookup(term): + """Books matching a title/author, to choose from; each has an ISBN so choosing one is a normal add.""" + term = re.sub(r"\s+", " ", term or "").strip()[:100] + if len(term) < 3: + return {"candidates": []} + found, seen = [], set() + for src in SOURCES: + got = [] + if src == "openlibrary": + got = openlibrary_candidates(term) + elif src.startswith("z3950:"): + got = z3950_candidates(src[len("z3950:"):], term) + for c in got: + key = (c["isbn"], (c["title"] + c["author"]).lower()[:40]) + if c["isbn"] in {k[0] for k in seen} or key in seen: + continue + seen.add(key) + found.append(c) + found = found[:12] + if found: # which of these do we already own? one query for all + conds = [{"isbn": {"-like": "%" + c["isbn"] + "%"}} for c in found] + [{"ean": {"-like": "%" + c["isbn"] + "%"}} for c in found] + try: + owned = " ".join((b.get("isbn") or "") + " " + (b.get("ean") or "") for b in + (koha("GET", "/biblios", query={"q": json.dumps({"-or": conds}), "_per_page": "50"}) or [])) + except KohaError: + owned = "" + for c in found: + alt = isbn13_to_10(c["isbn"]) or "" + c["have"] = c["isbn"] in owned or bool(alt and alt in owned) + have = found + return {"candidates": have} + + # ------------------------------------------------------------ photo scanning MAX_IMAGE = 12 * 1024 * 1024 ZBAR_TIMEOUT = 20 @@ -1726,6 +1821,12 @@ class Handler(BaseHTTPRequestHandler): except KohaError as e: return self._send(404 if e.status == 404 else 502, {"status": "error", "message": "Koha is not answering yet - try again in a minute." if e.status == 503 else "Failed: %s" % e.body[:120]}) + if path == "/api/lookup": + q = (urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query).get("q") or [""])[0] + try: + return self._send(200, op_lookup(q)) + except KohaError: + return self._send(502, {"status": "error", "message": "Koha is not answering yet - try again in a minute."}) if path == "/api/me": if self.role != "patron": return self._send(403, {"status": "error", "message": "Sign in with a library card to see your books."}) diff --git a/library/quickadd/index.html b/library/quickadd/index.html index bfc4ea2..2b68bc7 100644 --- a/library/quickadd/index.html +++ b/library/quickadd/index.html @@ -44,6 +44,34 @@ padding: 10px 16px; background: var(--card); color: var(--ink); font-weight: 600; } button.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); } button:focus-visible, a:focus-visible, input:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } + header.top { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 4px 0 8px; border-bottom: 1px solid var(--line); margin-bottom: 12px; } + .brand { display: flex; align-items: center; gap: 8px; text-decoration: none; color: var(--ink); } + .brand .logo { font-size: 1.5rem; } .brand h1 { margin: 0; } + .menubtn { font-size: 1.5rem; line-height: 1; padding: 6px 14px; } + .menu { position: absolute; right: 0; top: 100%; z-index: 20; background: var(--card); border: 1px solid var(--line); border-radius: 12px; + box-shadow: 0 8px 24px rgba(0,0,0,.18); padding: 8px; min-width: 250px; display: flex; flex-direction: column; gap: 2px; } + .menu[hidden] { display: none; } + .menu a { color: var(--ink); text-decoration: none; padding: 9px 12px; border-radius: 8px; font-weight: 600; } + .menu a:hover, .menu a:focus-visible { background: var(--bg); } + .menu a.on { background: var(--accent); color: var(--accent-ink); } + .menu a.ext::after { content: ' \2197'; font-size: .85em; } + .menu a[hidden] { display: none; } + .menugroup { border-top: 1px solid var(--line); margin-top: 6px; padding-top: 6px; display: flex; flex-direction: column; gap: 2px; } + .menugroup[hidden] { display: none; } + .menulabel { font-size: .8rem; color: var(--muted); padding: 2px 12px; text-transform: uppercase; letter-spacing: .05em; } + .find-form.big input { font-size: 1.5rem; padding: 16px 18px; } + .bigacts { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 16px 0; } + .bigacts[hidden] { display: none; } + .bigact { display: flex; flex-direction: column; gap: 4px; padding: 18px 14px; border-radius: 14px; background: var(--accent); color: var(--accent-ink); + text-decoration: none; min-height: 96px; justify-content: center; } + .bigact strong { font-size: 1.25rem; } .bigact span { font-size: .88rem; opacity: .9; } + .bigact:hover, .bigact:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } + .foot { max-width: 760px; margin: 8px auto 28px; padding: 14px 16px 0; border-top: 1px solid var(--line); color: var(--muted); font-size: .9rem; text-align: center; } + .foot a { color: var(--accent); } + .cand { display: flex; gap: 12px; align-items: flex-start; padding: 10px 0; border-bottom: 1px solid var(--line); } + .cand .cv { width: 54px; height: 78px; border-radius: 4px; background: var(--line); flex: none; object-fit: cover; } + .cand .t { flex: 1; min-width: 0; overflow-wrap: anywhere; } + @media print { .foot, .menubtn, .menu { display: none !important; } } nav.tabs { display: flex; flex-wrap: wrap; gap: 4px 6px; align-items: center; } nav.tabs a { color: var(--accent); text-decoration: none; font-weight: 600; padding: 6px 12px; border-radius: 999px; } nav.tabs a.on { background: var(--accent); color: var(--accent-ink); } @@ -134,7 +162,7 @@ .libcard .no { font-size: 9pt; text-align: center; letter-spacing: .15em; } .cardsheet { display: grid; grid-template-columns: repeat(2, 85.6mm); gap: 6mm; justify-content: start; } .cardsheet .libcard { break-inside: avoid; } - @media print { header, .noprint, nav { display: none !important; } body { background: #fff; } main { padding: 0; max-width: none; } .libcard { border: 1px solid #000; } } + @media print { header, .noprint, nav, .foot { display: none !important; } body { background: #fff; } main { padding: 0; max-width: none; } .libcard { border: 1px solid #000; } } #live { position: fixed; inset: 0; z-index: 50; background: rgba(0,0,0,.92); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 12px; } #live[hidden] { display: none; } #live video { width: min(100%, 640px); max-height: 60vh; border-radius: 12px; background: #000; object-fit: cover; } @@ -146,19 +174,48 @@
-
-

Room for Books

-
+