A library holds both articles and questions now. It held only questions, so the bookmark on an article had nowhere to write and stood in for the questions filed under the topic instead — which is not what a reader who saved the reading asked for, and left a topic with no questions unsaveable. Its own table rather than a nullable column beside `question_id`: that shape allows a row with both or neither, and every read then has to say which kind it is looking at. Which libraries already hold an article is now asked of the server, as one question. It was kept on the device because the API could not answer, which was wrong on the second machine and silently so. Putting one back is the same control rather than an undo somewhere else. "Short" is called Summary, because that is what the section is called, and it is a toggle rather than one tab of three — the whole topic, or the part of it worth revising, which is a different kind of choice from Long versus Clinical. It names its own state, so a reader can tell why two thirds of the contents are not there. The stored variant stays `short`: renaming it would be a data migration to change a word on a button. Also: `litellm==1.28.13` has been withdrawn from PyPI, so requirements.txt could not be edited at all without the pip layer failing to rebuild — which is what blocked pinning Pillow. Repinned to 1.53.1, the nearest still published; the three things we use are unchanged in it, and both suites pass on the new set. Pillow is pinned properly now rather than arriving through PyMuPDF. One consequence, handled: `litellm.utils.get_valid_models()` now returns nothing unless a provider's own API key is in the environment, and ours is a proxy. That branch is only reached when no proxy is configured, and it now says so instead of answering with an empty list that reads as "this site has no models". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
42 lines
1.9 KiB
Python
42 lines
1.9 KiB
Python
from datetime import datetime
|
|
|
|
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, UniqueConstraint
|
|
from app.database import Base
|
|
|
|
|
|
class UserCollection(Base):
|
|
__tablename__ = "user_collections"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
title = Column(String(200), nullable=False)
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
# When it was last opened or added to. Null means never since this was
|
|
# recorded, which is not the same claim as "never used".
|
|
last_used_at = Column(DateTime, nullable=True)
|
|
|
|
|
|
class UserCollectionQuestion(Base):
|
|
__tablename__ = "user_collection_questions"
|
|
__table_args__ = (UniqueConstraint("collection_id", "question_id", name="uq_collection_question"),)
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
collection_id = Column(Integer, ForeignKey("user_collections.id", ondelete="CASCADE"), nullable=False)
|
|
question_id = Column(Integer, ForeignKey("questions.id", ondelete="CASCADE"), nullable=False)
|
|
|
|
|
|
class UserCollectionArticle(Base):
|
|
"""An article put aside into a library.
|
|
|
|
Its own table rather than a nullable `article_id` beside `question_id` on
|
|
the row above: that shape allows a row with both, or with neither, and
|
|
every read then has to say which kind it is looking at. Two tables, one
|
|
unique constraint each, and a library is the union of them.
|
|
"""
|
|
|
|
__tablename__ = "user_collection_articles"
|
|
__table_args__ = (UniqueConstraint("collection_id", "article_id", name="uq_collection_article"),)
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
collection_id = Column(Integer, ForeignKey("user_collections.id", ondelete="CASCADE"), nullable=False)
|
|
article_id = Column(Integer, ForeignKey("articles.id", ondelete="CASCADE"), nullable=False)
|