Fix contact filtering and compact workspace alerts

This commit is contained in:
2026-08-04 08:21:49 +02:00
parent b3ca069644
commit f1a2952d83
4 changed files with 41 additions and 10 deletions
+3
View File
@@ -28,6 +28,9 @@ imports, synchronization, quality review, and reversible merge operations.
reads never infer deletions. reads never infer deletions.
- Merge and communication-governance operations append auditable evidence and - Merge and communication-governance operations append auditable evidence and
never silently erase prior state. never silently erase prior state.
- Request feedback is rendered as a compact shared alert over the full-height
workspace. It does not become a grid row or displace the source, contact, and
detail columns.
Backend and WebUI manifests publish matching route/section/action surfaces. Backend and WebUI manifests publish matching route/section/action surfaces.
English and German catalogues include the owned interaction vocabulary; major English and German catalogues include the owned interaction vocabulary; major
+11 -6
View File
@@ -12,7 +12,7 @@ import unicodedata
import urllib.parse import urllib.parse
from typing import Any from typing import Any
from sqlalchemy import and_, false, func, or_ from sqlalchemy import and_, exists, false, func, or_
from sqlalchemy.orm import Session, selectinload from sqlalchemy.orm import Session, selectinload
from govoplan_core.auth import ApiPrincipal from govoplan_core.auth import ApiPrincipal
@@ -3256,21 +3256,26 @@ def _filtered_contact_query(
) )
if address_book_id and address_list.address_book_id != address_book_id: 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.") raise AddressBookError("Address list does not belong to the selected address book.")
contact_query = contact_query.join( contact_query = contact_query.filter(
AddressListEntry, exists().where(
AddressListEntry.contact_id == Contact.id, AddressListEntry.contact_id == Contact.id,
).filter(AddressListEntry.address_list_id == address_list_id) AddressListEntry.address_list_id == address_list_id,
)
)
normalized_query = _trim(query) normalized_query = _trim(query)
if normalized_query: if normalized_query:
pattern = f"%{normalized_query.lower()}%" pattern = f"%{normalized_query.lower()}%"
contact_query = contact_query.outerjoin(ContactEmail).filter( contact_query = contact_query.filter(
or_( or_(
func.lower(Contact.display_name).like(pattern), func.lower(Contact.display_name).like(pattern),
func.lower(Contact.organization).like(pattern), func.lower(Contact.organization).like(pattern),
exists().where(
ContactEmail.contact_id == Contact.id,
func.lower(ContactEmail.email).like(pattern), func.lower(ContactEmail.email).like(pattern),
),
) )
) )
return contact_query.distinct() return contact_query
def get_visible_contact(session: Session, principal: ApiPrincipal, contact_id: str, *, include_deleted: bool = False) -> Contact: def get_visible_contact(session: Session, principal: ApiPrincipal, contact_id: str, *, include_deleted: bool = False) -> Contact:
+23
View File
@@ -6,6 +6,7 @@ from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
from sqlalchemy import create_engine, inspect from sqlalchemy import create_engine, inspect
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from govoplan_core.core.change_sequence import ChangeSequenceEntry from govoplan_core.core.change_sequence import ChangeSequenceEntry
@@ -142,6 +143,7 @@ from govoplan_addresses.backend.service import (
suggest_duplicate_contacts, suggest_duplicate_contacts,
resolve_trusted_deployment_carddav_credential_ref, resolve_trusted_deployment_carddav_credential_ref,
_carddav_client_for_source, _carddav_client_for_source,
_filtered_contact_query,
) )
@@ -309,6 +311,27 @@ class AddressServiceTest(unittest.TestCase):
self.session.commit() self.session.commit()
self.assertEqual([item.id for item in list_contacts(self.session, self.principal, address_book_id=book.id)], [contact.id]) self.assertEqual([item.id for item in list_contacts(self.session, self.principal, address_book_id=book.id)], [contact.id])
def test_contact_query_is_postgresql_json_safe(self) -> None:
book = create_address_book(
self.session,
self.principal,
AddressBookCreateRequest(scope_type="user", name="PostgreSQL-safe"),
)
self.session.commit()
contact_query = _filtered_contact_query(
self.session,
self.principal,
address_book_id=book.id,
address_list_id=None,
query="example",
include_deleted=False,
)
compiled = str(contact_query.statement.compile(dialect=postgresql.dialect()))
self.assertNotIn("SELECT DISTINCT", compiled.upper())
self.assertIn("EXISTS", compiled.upper())
def test_contact_and_relationship_routes_emit_value_free_audit_evidence(self) -> None: def test_contact_and_relationship_routes_emit_value_free_audit_evidence(self) -> None:
book = create_address_book( book = create_address_book(
self.session, self.session,
@@ -2978,8 +2978,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
return ( return (
<div className="workspace-data-page module-entry-page address-book-page address-book-fullscreen"> <div className="workspace-data-page module-entry-page address-book-page address-book-fullscreen">
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>} {error && <DismissibleAlert className="address-error" compact tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{notice && !error && <DismissibleAlert tone="success" resetKey={notice}>{notice}</DismissibleAlert>} {notice && !error && <DismissibleAlert className="address-error" compact tone="success" resetKey={notice}>{notice}</DismissibleAlert>}
{!canWriteBooks && !canWriteLists && !canWriteContacts && <ActionBlockerHint {!canWriteBooks && !canWriteLists && !canWriteContacts && <ActionBlockerHint
tone="info" tone="info"
reason={{ reason={{