Manual vs. Smart Collections
In this project, the Collection model (found in app/models/collection.py) provides two distinct ways to group bookmarks: Manual and Smart. This design allows users to either curate specific lists of bookmarks or define dynamic rules that automatically aggregate content based on metadata.
The behavior of a collection is determined by its CollectionType enum:
class CollectionType(Enum):
"""The kind of collection."""
MANUAL = "manual"
SMART = "smart"
Manual Collections: Explicit Curation
Manual collections are the default behavior. They function as an ordered list of bookmark IDs that the user manages explicitly. In this mode, the bookmark_ids attribute is the primary source of truth, and the system provides methods to mutate this list directly.
The Collection class enforces manual management through its public API:
def add_bookmark(self, bookmark_id: str) -> bool:
"""Add a bookmark to a manual collection."""
if self.is_smart or bookmark_id in self.bookmark_ids:
return False
self.bookmark_ids.append(bookmark_id)
return True
Key characteristics of manual collections include:
- Explicit Control: Bookmarks are only added or removed via direct user action (e.g., calling
add_bookmarkorremove_bookmark). - Custom Ordering: Users can define a specific sequence for bookmarks using the
reordermethod, which validates that the new list contains the exact same set of IDs as the current collection. - Persistence: The list of IDs is stored directly in the
bookmark_idsfield and persisted via theBookmarkRepository.
Smart Collections: Rule-Based Automation
Smart collections shift the responsibility of membership from the user to the system. Instead of a static list of IDs, a smart collection uses a filter_rule to determine which bookmarks belong to it.
The logic for this automation is encapsulated in the _apply_filter method:
def _apply_filter(self, bookmarks: list) -> List[str]:
"""Evaluate the filter_rule against a list of bookmarks."""
if not self.filter_rule:
return []
keyword = self.filter_rule.lower()
return [b.id for b in bookmarks if keyword in b.title.lower() or keyword in b.description.lower()]
In the current implementation, "smart" matching is a simple case-insensitive substring search performed against the title and description of bookmarks.
Design Trade-offs and Constraints
The implementation makes several specific design choices that impact how collections are used:
1. Mutual Exclusivity
A collection cannot be both manual and smart. This is enforced in the add_bookmark method, which returns False if is_smart is true. This prevents "polluting" a rule-based collection with manual overrides, ensuring that the filter_rule remains the definitive source of truth for smart collections.
2. Internal Filtering Logic
The _apply_filter method is marked as internal. While the model defines how filtering should work, the current architecture (as seen in app/services/bookmark_service.py) delegates the actual population of these IDs to the service layer. This separation allows the model to remain a "dumb" data container while the service layer handles the complexity of fetching all bookmarks to run them through the filter.
3. Reordering Constraints
The reorder method is strictly for changing the sequence of existing items:
def reorder(self, bookmark_ids: List[str]) -> None:
"""Replace the bookmark ordering."""
if set(bookmark_ids) != set(self.bookmark_ids):
raise ValueError("Reorder list must contain exactly the same bookmark IDs")
self.bookmark_ids = bookmark_ids
This constraint ensures that reordering cannot be used as a side-channel for adding or removing bookmarks, maintaining the integrity of the collection's membership logic.
4. Serialization and Initialization
The from_dict class method provides a default type of MANUAL if none is specified. However, it is important to note that from_dict does not automatically trigger _apply_filter. When a smart collection is created via the API (in app/routes/collections.py), it is initialized with a filter_rule, but its bookmark_ids list remains empty until the service layer evaluates the rule against the bookmark library.
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Collection":
"""Construct from a dictionary."""
ctype = CollectionType(data.get("type", "manual"))
return cls(
name=data["name"],
collection_type=ctype,
filter_rule=data.get("filter_rule", ""),
)
This design choice keeps the model instantiation fast and decoupled from the database, as the model itself does not have access to the full list of bookmarks required to populate a smart collection.