Paginate address book contacts
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user