Paginate address book contacts

This commit is contained in:
2026-07-30 05:22:09 +02:00
parent 1545ea711e
commit bf1d7c9678
7 changed files with 231 additions and 16 deletions

View File

@@ -80,6 +80,7 @@ from govoplan_addresses.backend.service import (
create_carddav_sync_source,
create_contact,
create_sync_source,
count_contacts,
delete_address_book,
delete_address_list,
delete_address_list_entry,
@@ -430,16 +431,41 @@ def api_restore_address_book(
@router.get("/contacts", response_model=ContactListResponse)
def api_list_contacts(
address_book_id: str | None = Query(default=None),
address_list_id: str | None = Query(default=None),
query: str | None = Query(default=None),
limit: int = Query(default=200, ge=1, le=500),
offset: int = Query(default=0, ge=0),
include_deleted: bool = Query(default=False),
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "addresses:contact:read")
try:
contacts = list_contacts(session, principal, address_book_id=address_book_id, query=query, limit=limit, include_deleted=include_deleted)
return ContactListResponse(contacts=[_contact_response(contact) for contact in contacts])
contacts = list_contacts(
session,
principal,
address_book_id=address_book_id,
address_list_id=address_list_id,
query=query,
limit=limit,
offset=offset,
include_deleted=include_deleted,
)
total = count_contacts(
session,
principal,
address_book_id=address_book_id,
address_list_id=address_list_id,
query=query,
include_deleted=include_deleted,
)
return ContactListResponse(
contacts=[_contact_response(contact) for contact in contacts],
total=total,
offset=offset,
limit=limit,
has_more=offset + len(contacts) < total,
)
except AddressBookError as exc:
raise _error(exc) from exc

View File

@@ -221,6 +221,10 @@ class ContactResponse(BaseModel):
class ContactListResponse(BaseModel):
contacts: list[ContactResponse]
total: int
offset: int
limit: int
has_more: bool
class AddressLookupResponse(BaseModel):

View File

@@ -2421,14 +2421,79 @@ def list_contacts(
principal: ApiPrincipal,
*,
address_book_id: str | None = None,
address_list_id: str | None = None,
query: str | None = None,
limit: int = 200,
offset: int = 0,
include_deleted: bool = False,
) -> list[Contact]:
contact_query = _filtered_contact_query(
session,
principal,
address_book_id=address_book_id,
address_list_id=address_list_id,
query=query,
include_deleted=include_deleted,
)
return (
contact_query.order_by(Contact.display_name.asc(), Contact.id.asc())
.offset(max(0, offset))
.limit(max(1, min(limit, 500)))
.all()
)
def count_contacts(
session: Session,
principal: ApiPrincipal,
*,
address_book_id: str | None = None,
address_list_id: str | None = None,
query: str | None = None,
include_deleted: bool = False,
) -> int:
contact_query = _filtered_contact_query(
session,
principal,
address_book_id=address_book_id,
address_list_id=address_list_id,
query=query,
include_deleted=include_deleted,
)
return int(
contact_query.order_by(None)
.with_entities(func.count(func.distinct(Contact.id)))
.scalar()
or 0
)
def _filtered_contact_query(
session: Session,
principal: ApiPrincipal,
*,
address_book_id: str | None,
address_list_id: str | None,
query: str | None,
include_deleted: bool,
):
contact_query = _visible_contact_query(session, principal, include_deleted=include_deleted, include_deleted_books=include_deleted)
if address_book_id:
get_visible_address_book(session, principal, address_book_id, include_deleted=include_deleted)
contact_query = contact_query.filter(Contact.address_book_id == address_book_id)
if address_list_id:
address_list = get_visible_address_list(
session,
principal,
address_list_id,
include_deleted=include_deleted,
)
if address_book_id and address_list.address_book_id != address_book_id:
raise AddressBookError("Address list does not belong to the selected address book.")
contact_query = contact_query.join(
AddressListEntry,
AddressListEntry.contact_id == Contact.id,
).filter(AddressListEntry.address_list_id == address_list_id)
normalized_query = _trim(query)
if normalized_query:
pattern = f"%{normalized_query.lower()}%"
@@ -2439,7 +2504,7 @@ def list_contacts(
func.lower(ContactEmail.email).like(pattern),
)
)
return contact_query.order_by(Contact.display_name.asc(), Contact.id.asc()).limit(max(1, min(limit, 500))).all()
return contact_query.distinct()
def get_visible_contact(session: Session, principal: ApiPrincipal, contact_id: str, *, include_deleted: bool = False) -> Contact: