Source code for flexicon.code.Notebook.annotation_collection

#
#   annotation_collection.py
#
#   Class: AnnotationCollection
#          Smart collection for annotations with type-aware filtering
#          and unified access across definition-based types.
#
#   Platform: Python.NET
#             FieldWorks Version 9+
#
#   Copyright 2025
#

"""
Smart collection class for annotations.

This module provides AnnotationCollection, a smart collection that manages
annotations while showing type diversity and supporting unified operations
across different annotation types.

Problem:
    GetAll() returns annotations with different types (defined by AnnotationDefn).
    Users need to:
    - See which types are represented
    - Filter by type if they want to
    - Work with all types together without manual type checking
    - Filter by metadata (author, date range, content)

Solution:
    AnnotationCollection provides:
    - __str__() showing type breakdown by AnnotationDefn
    - by_type() filtering to specific annotation types
    - filter() for common criteria (annotation_type, author, date_created)
    - Convenience methods (scripture_annotations(), translator_notes(), etc.)
    - Chainable filtering: annos.scripture().filter(author='John')

Example::

    from flexlibs2.code.Notebook.annotation_collection import AnnotationCollection

    # GetAll() returns AnnotationCollection
    annos = noteOps.GetAll(entry)
    print(annos)  # Shows type breakdown
    # AnnotationCollection (12 total)
    #   To Do: 5 (42%)
    #   Question: 4 (33%)
    #   ScrScriptureNote: 3 (25%)

    # Filter by type
    todos = annos.by_type("To Do")
    print(len(todos))  # 5

    # Filter by author
    john_notes = annos.filter(author="John")

    # Filter by date
    from System import DateTime
    one_week_ago = DateTime.Now.AddDays(-7)
    recent = annos.filter(since=one_week_ago)

    # Chain filters
    john_todos = annos.by_type("To Do").filter(author="John")

    # Use convenience methods
    scripture = annos.scripture()
    translator = annos.translator()

    # Iterate
    for anno in annos:
        print(f"{anno.annotation_type}: {anno.content}")
"""

from ..Shared.smart_collection import SmartCollection
from .annotation import Annotation

try:
    from System import DateTime
except ImportError:
    # Mock for testing
    from datetime import datetime

    class DateTime:
        def __init__(self, year=2025, month=1, day=1, hour=0, minute=0, second=0):
            self.dt = datetime(year, month, day, hour, minute, second)

        def AddDays(self, days):
            result = DateTime()
            import datetime as dt_module

            result.dt = self.dt + dt_module.timedelta(days=days)
            return result

        @classmethod
        def Now(cls):
            now = datetime.now()
            return cls(now.year, now.month, now.day, now.hour, now.minute, now.second)

        def __lt__(self, other):
            return self.dt < other.dt

        def __le__(self, other):
            return self.dt <= other.dt

        def __gt__(self, other):
            return self.dt > other.dt

        def __ge__(self, other):
            return self.dt >= other.dt

        def __eq__(self, other):
            if isinstance(other, DateTime):
                return self.dt == other.dt
            return False


[docs] class AnnotationCollection(SmartCollection): """ Smart collection for annotations with type-aware filtering. Manages collections of annotation (Annotation wrapper objects) with type-aware display and filtering capabilities. Supports filtering by annotation type, author, date, and custom predicates. Attributes: _items: List of Annotation wrapper objects Example:: annos = noteOps.GetAll(entry) # Returns AnnotationCollection print(annos) # Shows type breakdown todos = annos.by_type("To Do") # Filter to To Do john = annos.filter(author="John") # Filter by author both = annos.by_type("To Do").filter(author="John") # Chain """ def __init__(self, items=None): """ Initialize an AnnotationCollection. Args: items: Iterable of Annotation objects, or None for empty. Example:: collection = AnnotationCollection() collection = AnnotationCollection([anno1, anno2, anno3]) """ super().__init__(items)
[docs] def filter(self, annotation_type=None, author=None, since=None, until=None, content_contains=None, where=None): """ Filter the collection by annotation properties. Supports filtering by properties that work across all annotation types (annotation_type, author, date range, content). For complex filtering, use where(). Args: annotation_type (str, optional): Filter to annotations with this type. Case-sensitive match of AnnotationDefn.Name. author (str, optional): Filter to annotations by this author. Case-sensitive match of Source property. since (System.DateTime, optional): Filter to annotations created on or after this date. until (System.DateTime, optional): Filter to annotations created on or before this date. content_contains (str, optional): Filter to annotations whose content contains this string (case-sensitive). where (callable, optional): Custom predicate function. If provided, other criteria are ignored. Receives Annotation wrapper object. Returns: AnnotationCollection: New collection with filtered items. Example:: # Filter by annotation type todos = annos.filter(annotation_type="To Do") # Filter by author john_notes = annos.filter(author="John") # Filter by date range from System import DateTime start = DateTime(2025, 1, 1) end = DateTime(2025, 12, 31) year_notes = annos.filter(since=start, until=end) # Filter by content etymology = annos.filter(content_contains="etymology") # Custom filtering recent_todos = annos.where( lambda a: a.annotation_type == "To Do" and a.date_created and a.date_created > DateTime.Now.AddDays(-7) ) # Chain filters john_todos = annos.filter(author="John").filter(annotation_type="To Do") Notes: - All criteria are AND-ed together if multiple provided - String comparisons are case-sensitive - where() takes precedence over other criteria - Returns new collection (doesn't modify original) - Use where() for complex custom filtering """ # If custom predicate provided, use it if where is not None: filtered = [a for a in self._items if where(a)] return AnnotationCollection(filtered) # Apply criteria filters filtered = [] for anno in self._items: # Check annotation_type if annotation_type is not None: if anno.annotation_type != annotation_type: continue # Check author if author is not None: if anno.author != author: continue # Check since (created on or after) if since is not None: if anno.date_created is None or anno.date_created < since: continue # Check until (created on or before) if until is not None: if anno.date_created is None or anno.date_created > until: continue # Check content contains if content_contains is not None: if content_contains not in anno.content: continue # All criteria passed filtered.append(anno) return AnnotationCollection(filtered)
[docs] def by_type(self, annotation_type): """ Filter the collection to a specific annotation type. Returns a new collection containing only annotations of the specified type. Args: annotation_type (str): The annotation type name to filter by. Case-sensitive match of AnnotationDefn.Name. Returns: AnnotationCollection: New collection with filtered items. Example:: todos = annos.by_type("To Do") print(len(todos)) # Number of To Do annotations questions = annos.by_type("Question") Notes: - Type comparison is case-sensitive - Returns empty collection if no matching types - Equivalent to filter(annotation_type=type_name) """ return self.filter(annotation_type=annotation_type)
[docs] def by_author(self, author): """ Filter the collection to annotations by a specific author. Returns a new collection containing only annotations with the specified author. Args: author (str): The author name to filter by. Case-sensitive match of Source property. Returns: AnnotationCollection: New collection with filtered items. Example:: john_notes = annos.by_author("John") print(len(john_notes)) # Number of annotations by John Notes: - Author comparison is case-sensitive - Returns empty collection if no matching authors - Equivalent to filter(author=author_name) """ return self.filter(author=author)
[docs] def created_since(self, date): """ Filter the collection to annotations created on or after a date. Args: date (System.DateTime): The cutoff date (inclusive). Returns: AnnotationCollection: New collection with filtered items. Example:: from System import DateTime one_week_ago = DateTime.Now.AddDays(-7) recent = annos.created_since(one_week_ago) Notes: - Date comparison is inclusive (>= date) - Returns annotations without DateCreated in results """ return self.filter(since=date)
[docs] def created_until(self, date): """ Filter the collection to annotations created on or before a date. Args: date (System.DateTime): The cutoff date (inclusive). Returns: AnnotationCollection: New collection with filtered items. Example:: from System import DateTime end_of_year = DateTime(2025, 12, 31) old_notes = annos.created_until(end_of_year) Notes: - Date comparison is inclusive (<= date) """ return self.filter(until=date)
# ========== Convenience Type Filters ==========
[docs] def scripture(self): """ Filter to Scripture annotations. Returns a new collection containing only Scripture-type annotations (types containing 'Scripture' or 'Scr'). Returns: AnnotationCollection: New collection with Scripture annotations. Example:: scripture_notes = annos.scripture() print(len(scripture_notes)) # Number of Scripture notes Notes: - Uses the Annotation.is_scripture property - Case-insensitive type matching """ filtered = [a for a in self._items if a.is_scripture] return AnnotationCollection(filtered)
[docs] def translator(self): """ Filter to translator notes. Returns a new collection containing only translator-type annotations (types containing 'Translator'). Returns: AnnotationCollection: New collection with translator notes. Example:: translator_notes = annos.translator() print(len(translator_notes)) # Number of translator notes Notes: - Uses the Annotation.is_translator property - Case-insensitive type matching """ filtered = [a for a in self._items if a.is_translator] return AnnotationCollection(filtered)
[docs] def consultant(self): """ Filter to consultant notes. Returns a new collection containing only consultant-type annotations (types containing 'Consultant'). Returns: AnnotationCollection: New collection with consultant notes. Example:: consultant_notes = annos.consultant() print(len(consultant_notes)) # Number of consultant notes Notes: - Uses the Annotation.is_consultant property - Case-insensitive type matching """ filtered = [a for a in self._items if a.is_consultant] return AnnotationCollection(filtered)
[docs] def todos(self): """ Filter to "To Do" annotations. Returns a new collection containing only "To Do" type annotations. Returns: AnnotationCollection: New collection with To Do annotations. Example:: todos = annos.todos() print(len(todos)) # Number of To Do items Notes: - Uses the Annotation.is_todo property - Case-insensitive type matching """ filtered = [a for a in self._items if a.is_todo] return AnnotationCollection(filtered)
[docs] def questions(self): """ Filter to question annotations. Returns a new collection containing only question-type annotations (types containing 'Question' or 'Query'). Returns: AnnotationCollection: New collection with question annotations. Example:: questions = annos.questions() print(len(questions)) # Number of questions Notes: - Uses the Annotation.is_question property - Case-insensitive type matching """ filtered = [a for a in self._items if a.is_question] return AnnotationCollection(filtered)
[docs] def with_replies(self): """ Filter to annotations that have replies. Returns a new collection containing only annotations with at least one reply. Returns: AnnotationCollection: New collection with annotations that have replies. Example:: threaded = annos.with_replies() print(len(threaded)) # Number of annotations with discussion threads Notes: - Uses the Annotation.has_replies property """ filtered = [a for a in self._items if a.has_replies] return AnnotationCollection(filtered)
# ========== Collection Information ==========
[docs] def get_types(self): """ Get all unique annotation types in the collection. Returns: list: List of unique annotation type names (strings). Example:: types = annos.get_types() for type_name in types: print(f"Type: {type_name}") # Type: To Do # Type: Question # Type: ScrScriptureNote Notes: - Returns unique type names only - Order is based on first appearance in collection """ seen = set() types = [] for anno in self._items: anno_type = anno.annotation_type if anno_type and anno_type not in seen: seen.add(anno_type) types.append(anno_type) return types
[docs] def get_authors(self): """ Get all unique authors in the collection. Returns: list: List of unique author names (strings). Example:: authors = annos.get_authors() for author in authors: print(f"Author: {author}") Notes: - Returns unique author names only - Includes empty strings if any annotations have no author set - Order is based on first appearance in collection """ seen = set() authors = [] for anno in self._items: author = anno.author if author and author not in seen: seen.add(author) authors.append(author) return authors
def __str__(self): """ String representation showing annotation type breakdown. Shows the total count and breakdown by annotation type. Returns: str: Multi-line string showing type distribution. Example:: print(annos) # AnnotationCollection (12 total) # To Do: 5 (42%) # Question: 4 (33%) # ScrScriptureNote: 3 (25%) Notes: - Shows percentage of each type - Types are sorted by count (highest first) - Empty collection shows "(0 total)" """ if not self._items: return "AnnotationCollection (0 total)" # Count by type type_counts = {} for anno in self._items: anno_type = anno.annotation_type if not anno_type: anno_type = "[Untyped]" type_counts[anno_type] = type_counts.get(anno_type, 0) + 1 # Sort by count (highest first) sorted_types = sorted(type_counts.items(), key=lambda x: x[1], reverse=True) # Build display string lines = [f"AnnotationCollection ({len(self._items)} total)"] for anno_type, count in sorted_types: percentage = (count * 100) // len(self._items) if self._items else 0 lines.append(f" {anno_type}: {count} ({percentage}%)") return "\n".join(lines) def __repr__(self): """Brief representation.""" if not self._items: return "AnnotationCollection()" return f"AnnotationCollection({len(self._items)} items, types: {', '.join(self.get_types()[:3])}{'...' if len(self.get_types()) > 3 else ''})"