Grouping with Collections
In this project, the Collection model provides a way to group bookmarks beyond simple tagging. Collections are defined in app/models/collection.py and support two distinct modes of operation: manual curation and automated "smart" filtering.
Collection Types
The behavior of a collection is determined by its CollectionType enum:
- MANUAL: The default type. Bookmarks are added and removed explicitly by the user.
- SMART: Bookmarks are intended to be included automatically based on a
filter_rule.
The Collection class is implemented as a dataclass, ensuring a consistent structure for both types:
@dataclass
class Collection:
name: str
collection_type: CollectionType = CollectionType.MANUAL
bookmark_ids: List[str] = field(default_factory=list)
filter_rule: str = ""
is_pinned: bool = False
id: str = field(default_factory=lambda: uuid.uuid4().hex[:10])
created_at: datetime = field(default_factory=datetime.utcnow)
Manual Curation
Manual collections rely on the bookmark_ids attribute, which stores an ordered list of bookmark identifiers. The model provides several methods to manage this list safely.
Adding and Removing Bookmarks
The add_bookmark method includes a safeguard to prevent manual modification of smart collections. It returns False if the collection is smart or if the bookmark is already present.
def add_bookmark(self, bookmark_id: str) -> bool:
if self.is_smart or bookmark_id in self.bookmark_ids:
return False
self.bookmark_ids.append(bookmark_id)
return True
Similarly, remove_bookmark handles the removal of IDs from the internal list.
Ordering
Unlike tags, collections maintain a specific order for their bookmarks. The reorder method allows for updating this sequence, but it enforces strict validation: the new list must contain exactly the same set of IDs as the existing one.
def reorder(self, bookmark_ids: List[str]) -> None:
if set(bookmark_ids) != set(self.bookmark_ids):
raise ValueError("Reorder list must contain exactly the same bookmark IDs")
self.bookmark_ids = bookmark_ids
Smart Collections and Filtering
Smart collections use a filter_rule (a simple string keyword) to determine which bookmarks belong in the group.
Filter Logic
The internal _apply_filter method defines how these rules are evaluated. It performs a case-insensitive search for the keyword within both the bookmark's title and its description:
def _apply_filter(self, bookmarks: list) -> List[str]:
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()]
Implementation Status
While the Collection model contains the logic for filtering, the current implementation of BookmarkService (in app/services/bookmark_service.py) does not yet invoke _apply_filter during bookmark retrieval or creation. Consequently, smart collections in the current codebase do not automatically populate their bookmark_ids list.
Service Integration
The BookmarkService acts as the primary interface for managing collections, orchestrating interactions between the model and the BookmarkRepository.
Creating Collections
When creating a collection via the service, the from_dict class method is used to instantiate the model from raw input data:
# app/services/bookmark_service.py
def create_collection(self, data: Dict[str, Any]) -> Tuple[Optional[Collection], Optional[str]]:
name = data.get("name", "").strip()
if not name:
return None, "Collection name is required"
collection = Collection.from_dict(data)
self._repo.save_collection(collection)
return collection, None
Persistence Flow
The service layer ensures that any changes made to a collection's membership are persisted back to the repository. For example, when adding a bookmark:
- The service retrieves the
Collectionobject from the repository. - It calls
collection.add_bookmark(bookmark_id). - If successful, it calls
self._repo.save_collection(collection)to update the underlying storage.
This pattern is visible in add_to_collection and remove_from_collection within app/services/bookmark_service.py.
API Representation
Collections are serialized for the API using the to_dict method. This method includes a calculated size property (the length of bookmark_ids) and converts the CollectionType enum and datetime objects into JSON-compatible formats:
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"type": self.collection_type.value,
"bookmark_ids": self.bookmark_ids,
"filter_rule": self.filter_rule,
"is_pinned": self.is_pinned,
"size": self.size,
"created_at": self.created_at.isoformat(),
}