from __future__ import annotations from datetime import datetime from sqlalchemy import distinct, func, select from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import Publication, ScholarProfile, ScholarPublication from app.services.publications.modes import ( MODE_ALL, MODE_LATEST, MODE_UNREAD, resolve_publication_view_mode, ) from app.services.publications.queries import get_latest_run_id_for_user async def count_for_user( db_session: AsyncSession, *, user_id: int, mode: str = MODE_ALL, scholar_profile_id: int | None = None, favorite_only: bool = False, search: str | None = None, snapshot_before: datetime | None = None, ) -> int: resolved_mode = resolve_publication_view_mode(mode) latest_run_id = await get_latest_run_id_for_user(db_session, user_id=user_id) stmt = ( select(func.count(distinct(ScholarPublication.publication_id))) .select_from(ScholarPublication) .join(ScholarProfile, ScholarProfile.id == ScholarPublication.scholar_profile_id) .join(Publication, Publication.id == ScholarPublication.publication_id) .where(ScholarProfile.user_id == user_id) ) stmt = _apply_search_filter(stmt, search=search) if scholar_profile_id is not None: stmt = stmt.where(ScholarProfile.id == scholar_profile_id) if favorite_only: stmt = stmt.where(ScholarPublication.is_favorite.is_(True)) if snapshot_before is not None: stmt = stmt.where(ScholarPublication.created_at <= snapshot_before) if resolved_mode == MODE_UNREAD: stmt = stmt.where(ScholarPublication.is_read.is_(False)) if resolved_mode == MODE_LATEST: if latest_run_id is None: return 0 stmt = stmt.where(ScholarPublication.first_seen_run_id == latest_run_id) result = await db_session.execute(stmt) return int(result.scalar_one() or 0) def _apply_search_filter(stmt, *, search: str | None): if not search: return stmt safe_search = search.replace("%", r"\%").replace("_", r"\_") pattern = f"%{safe_search}%" return stmt.where( Publication.title_raw.ilike(pattern) | ScholarProfile.display_name.ilike(pattern) | Publication.venue_text.ilike(pattern) ) async def count_unread_for_user( db_session: AsyncSession, *, user_id: int, scholar_profile_id: int | None = None, favorite_only: bool = False, search: str | None = None, snapshot_before: datetime | None = None, ) -> int: return await count_for_user( db_session, user_id=user_id, mode=MODE_UNREAD, scholar_profile_id=scholar_profile_id, favorite_only=favorite_only, search=search, snapshot_before=snapshot_before, ) async def count_latest_for_user( db_session: AsyncSession, *, user_id: int, scholar_profile_id: int | None = None, favorite_only: bool = False, search: str | None = None, snapshot_before: datetime | None = None, ) -> int: return await count_for_user( db_session, user_id=user_id, mode=MODE_LATEST, scholar_profile_id=scholar_profile_id, favorite_only=favorite_only, search=search, snapshot_before=snapshot_before, ) async def count_favorite_for_user( db_session: AsyncSession, *, user_id: int, scholar_profile_id: int | None = None, search: str | None = None, snapshot_before: datetime | None = None, ) -> int: return await count_for_user( db_session, user_id=user_id, mode=MODE_ALL, scholar_profile_id=scholar_profile_id, favorite_only=True, search=search, snapshot_before=snapshot_before, )