Skip to main content

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:

  1. The service retrieves the Collection object from the repository.
  2. It calls collection.add_bookmark(bookmark_id).
  3. 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(),
}