Add author detail, idea detail, and gap-draft reverse link pages

- Author detail page (/authors/<person_id>): shows author info, all drafts
  with ratings, and co-authors with shared draft counts. Public route.
- Idea detail page (/ideas/<idea_id>): shows idea metadata, source draft,
  and top-5 most similar ideas via embedding cosine similarity. Admin route.
- Gap detail page: added "Related Drafts" section that finds drafts by
  extracting draft names from evidence text and searching by topic keywords.
- Updated author links across templates to use /authors/<person_id> URLs.
- Added DB methods: get_author_by_id, get_author_drafts, get_coauthors.
- Extended top_authors to include person_id (5th tuple element).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 03:45:00 +01:00
parent 4a368bde62
commit c755b2bbf3
16 changed files with 548 additions and 18 deletions

View File

@@ -639,10 +639,10 @@ class Database:
def author_count(self) -> int:
return self.conn.execute("SELECT COUNT(*) FROM authors").fetchone()[0]
def top_authors(self, limit: int = 20) -> list[tuple[str, str, int, list[str]]]:
"""Return (name, affiliation, draft_count, [draft_names])."""
def top_authors(self, limit: int = 20) -> list[tuple[str, str, int, list[str], int]]:
"""Return (name, affiliation, draft_count, [draft_names], person_id)."""
rows = self.conn.execute(
"""SELECT a.name, a.affiliation, COUNT(da.draft_name) as cnt,
"""SELECT a.person_id, a.name, a.affiliation, COUNT(da.draft_name) as cnt,
GROUP_CONCAT(da.draft_name, '||') as drafts
FROM authors a
JOIN draft_authors da ON a.person_id = da.person_id
@@ -653,10 +653,50 @@ class Database:
).fetchall()
return [
(r["name"], r["affiliation"], r["cnt"],
r["drafts"].split("||") if r["drafts"] else [])
r["drafts"].split("||") if r["drafts"] else [],
r["person_id"])
for r in rows
]
def get_author_by_id(self, person_id: int) -> dict | None:
"""Return author info by person_id, or None if not found."""
row = self.conn.execute(
"SELECT * FROM authors WHERE person_id = ?", (person_id,)
).fetchone()
if not row:
return None
return {
"person_id": row["person_id"],
"name": row["name"],
"ascii_name": row["ascii_name"],
"affiliation": row["affiliation"],
"resource_uri": row["resource_uri"],
}
def get_author_drafts(self, person_id: int) -> list[str]:
"""Return draft names for a given author."""
rows = self.conn.execute(
"SELECT draft_name FROM draft_authors WHERE person_id = ? ORDER BY draft_name",
(person_id,),
).fetchall()
return [r["draft_name"] for r in rows]
def get_coauthors(self, person_id: int) -> list[dict]:
"""Return co-authors for a given person (authors on the same drafts)."""
rows = self.conn.execute(
"""SELECT DISTINCT a.person_id, a.name, a.affiliation, COUNT(DISTINCT da2.draft_name) as shared
FROM draft_authors da1
JOIN draft_authors da2 ON da1.draft_name = da2.draft_name AND da2.person_id != da1.person_id
JOIN authors a ON da2.person_id = a.person_id
WHERE da1.person_id = ?
GROUP BY a.person_id
ORDER BY shared DESC""",
(person_id,),
).fetchall()
return [{"person_id": r["person_id"], "name": r["name"],
"affiliation": r["affiliation"], "shared_drafts": r["shared"]}
for r in rows]
def top_orgs(self, limit: int = 20) -> list[tuple[str, int, int]]:
"""Return (org, author_count, draft_count)."""
rows = self.conn.execute(