Compare commits
2 Commits
75c9ece709
...
93dddbb8c5
| Author | SHA1 | Date | |
|---|---|---|---|
| 93dddbb8c5 | |||
| 04accaa206 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/addresses-webui",
|
"name": "@govoplan/addresses-webui",
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "webui/src/index.ts",
|
"main": "webui/src/index.ts",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.9",
|
"@govoplan/core-webui": "^0.1.11",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-addresses"
|
name = "govoplan-addresses"
|
||||||
version = "0.1.8"
|
version = "0.1.9"
|
||||||
description = "GovOPlaN reusable address and recipient-source module."
|
description = "GovOPlaN reusable address and recipient-source module."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
authors = [{ name = "GovOPlaN" }]
|
authors = [{ name = "GovOPlaN" }]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defusedxml>=0.7.1",
|
"defusedxml>=0.7.1",
|
||||||
"govoplan-core>=0.1.9",
|
"govoplan-core>=0.1.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ from sqlalchemy import func
|
|||||||
|
|
||||||
from govoplan_core.auth import ApiPrincipal
|
from govoplan_core.auth import ApiPrincipal
|
||||||
from govoplan_core.db.base import utcnow
|
from govoplan_core.db.base import utcnow
|
||||||
|
from govoplan_core.core.people import (
|
||||||
|
PeopleSearchGroup,
|
||||||
|
PersonSearchCandidate,
|
||||||
|
person_selection_key,
|
||||||
|
)
|
||||||
from govoplan_addresses.backend.db.models import AddressBook, AddressList, AddressListEntry, Contact, ContactEmail, ContactPhone
|
from govoplan_addresses.backend.db.models import AddressBook, AddressList, AddressListEntry, Contact, ContactEmail, ContactPhone
|
||||||
from govoplan_addresses.backend.schemas import ContactCreateRequest
|
from govoplan_addresses.backend.schemas import ContactCreateRequest
|
||||||
from govoplan_addresses.backend.service import (
|
from govoplan_addresses.backend.service import (
|
||||||
@@ -137,6 +142,60 @@ class AddressesLookupCapability:
|
|||||||
return tuple(candidates)
|
return tuple(candidates)
|
||||||
|
|
||||||
|
|
||||||
|
class AddressesPeopleSearchProvider:
|
||||||
|
"""Adapt visible address-book contacts to the shared people-search contract."""
|
||||||
|
|
||||||
|
def __init__(self, lookup: AddressesLookupCapability | None = None) -> None:
|
||||||
|
self._lookup = lookup or AddressesLookupCapability()
|
||||||
|
|
||||||
|
def search_people(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
query: str,
|
||||||
|
limit: int = 25,
|
||||||
|
) -> tuple[PeopleSearchGroup, ...]:
|
||||||
|
if not hasattr(principal, "account_id") or not hasattr(principal, "tenant_id") or not hasattr(principal, "group_ids"):
|
||||||
|
raise AddressBookError("Address contact search requires an authenticated tenant principal.")
|
||||||
|
candidates = self._lookup.lookup(
|
||||||
|
session,
|
||||||
|
principal, # type: ignore[arg-type] - validated principal contract
|
||||||
|
query=query,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
PeopleSearchGroup(
|
||||||
|
key="contacts",
|
||||||
|
label="Contacts",
|
||||||
|
candidates=tuple(
|
||||||
|
PersonSearchCandidate(
|
||||||
|
selection_key=person_selection_key("contact", item.contact_id, email=item.email),
|
||||||
|
kind="contact",
|
||||||
|
reference_id=item.contact_id,
|
||||||
|
display_name=item.display_name,
|
||||||
|
email=item.email,
|
||||||
|
source_module="addresses",
|
||||||
|
source_label="Contacts",
|
||||||
|
source_ref=item.source_ref or f"addresses:contact:{item.contact_id}",
|
||||||
|
source_revision=item.source_revision,
|
||||||
|
description=" · ".join(part for part in (item.organization, item.role_title) if part) or None,
|
||||||
|
provenance=dict(item.provenance),
|
||||||
|
metadata={
|
||||||
|
"address_book_id": item.address_book_id,
|
||||||
|
"email_label": item.email_label,
|
||||||
|
"organization": item.organization,
|
||||||
|
"role_title": item.role_title,
|
||||||
|
"tags": tuple(item.tags),
|
||||||
|
"source_kind": item.source_kind,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for item in candidates
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AddressesContactWriterCapability:
|
class AddressesContactWriterCapability:
|
||||||
def list_write_targets(
|
def list_write_targets(
|
||||||
self,
|
self,
|
||||||
@@ -301,6 +360,10 @@ def lookup_capability(_context: Any) -> AddressesLookupCapability:
|
|||||||
return AddressesLookupCapability()
|
return AddressesLookupCapability()
|
||||||
|
|
||||||
|
|
||||||
|
def people_search_capability(_context: Any) -> AddressesPeopleSearchProvider:
|
||||||
|
return AddressesPeopleSearchProvider()
|
||||||
|
|
||||||
|
|
||||||
def recipient_source_capability(_context: Any) -> AddressesRecipientSourceCapability:
|
def recipient_source_capability(_context: Any) -> AddressesRecipientSourceCapability:
|
||||||
return AddressesRecipientSourceCapability()
|
return AddressesRecipientSourceCapability()
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from govoplan_addresses.backend.capabilities import (
|
|||||||
from govoplan_addresses.backend.db import models as addresses_models # noqa: F401 - populate address ORM metadata
|
from govoplan_addresses.backend.db import models as addresses_models # noqa: F401 - populate address ORM metadata
|
||||||
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
||||||
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
|
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
|
||||||
|
from govoplan_core.core.people import CAPABILITY_ADDRESSES_PEOPLE_SEARCH
|
||||||
from govoplan_core.core.modules import (
|
from govoplan_core.core.modules import (
|
||||||
DocumentationTopic,
|
DocumentationTopic,
|
||||||
FrontendModule,
|
FrontendModule,
|
||||||
@@ -148,11 +149,12 @@ def _addresses_router(_context: ModuleContext):
|
|||||||
manifest = ModuleManifest(
|
manifest = ModuleManifest(
|
||||||
id="addresses",
|
id="addresses",
|
||||||
name="Addresses",
|
name="Addresses",
|
||||||
version="0.1.8",
|
version="0.1.9",
|
||||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||||
optional_dependencies=("campaigns", "mail", "forms", "reporting", "portal", "postbox"),
|
optional_dependencies=("campaigns", "mail", "forms", "reporting", "portal", "postbox"),
|
||||||
provides_interfaces=(
|
provides_interfaces=(
|
||||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_LOOKUP, version="0.1.8"),
|
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_LOOKUP, version="0.1.8"),
|
||||||
|
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_PEOPLE_SEARCH, version="0.1.0"),
|
||||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_RECIPIENT_SOURCE, version="0.1.8"),
|
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_RECIPIENT_SOURCE, version="0.1.8"),
|
||||||
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_CONTACT_WRITER, version="0.1.8"),
|
ModuleInterfaceProvider(name=CAPABILITY_ADDRESSES_CONTACT_WRITER, version="0.1.8"),
|
||||||
),
|
),
|
||||||
@@ -177,6 +179,7 @@ manifest = ModuleManifest(
|
|||||||
),
|
),
|
||||||
capability_factories={
|
capability_factories={
|
||||||
CAPABILITY_ADDRESSES_LOOKUP: lambda context: __import__("govoplan_addresses.backend.capabilities", fromlist=["lookup_capability"]).lookup_capability(context),
|
CAPABILITY_ADDRESSES_LOOKUP: lambda context: __import__("govoplan_addresses.backend.capabilities", fromlist=["lookup_capability"]).lookup_capability(context),
|
||||||
|
CAPABILITY_ADDRESSES_PEOPLE_SEARCH: lambda context: __import__("govoplan_addresses.backend.capabilities", fromlist=["people_search_capability"]).people_search_capability(context),
|
||||||
CAPABILITY_ADDRESSES_RECIPIENT_SOURCE: lambda context: __import__(
|
CAPABILITY_ADDRESSES_RECIPIENT_SOURCE: lambda context: __import__(
|
||||||
"govoplan_addresses.backend.capabilities",
|
"govoplan_addresses.backend.capabilities",
|
||||||
fromlist=["recipient_source_capability"],
|
fromlist=["recipient_source_capability"],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from sqlalchemy import create_engine, inspect
|
|||||||
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
|
||||||
|
from govoplan_core.core.people import CAPABILITY_ADDRESSES_PEOPLE_SEARCH, PeopleSearchProvider
|
||||||
from govoplan_core.db.base import Base
|
from govoplan_core.db.base import Base
|
||||||
from govoplan_core.db.base import utcnow
|
from govoplan_core.db.base import utcnow
|
||||||
from govoplan_addresses.backend.carddav import AddressCardDAVObject, AddressCardDAVReportResult, AddressCardDAVWriteResult
|
from govoplan_addresses.backend.carddav import AddressCardDAVObject, AddressCardDAVReportResult, AddressCardDAVWriteResult
|
||||||
@@ -16,6 +17,7 @@ from govoplan_addresses.backend.capabilities import (
|
|||||||
CAPABILITY_ADDRESSES_RECIPIENT_SOURCE,
|
CAPABILITY_ADDRESSES_RECIPIENT_SOURCE,
|
||||||
AddressesContactWriterCapability,
|
AddressesContactWriterCapability,
|
||||||
AddressesLookupCapability,
|
AddressesLookupCapability,
|
||||||
|
AddressesPeopleSearchProvider,
|
||||||
AddressesRecipientSourceCapability,
|
AddressesRecipientSourceCapability,
|
||||||
AddressWriterError,
|
AddressWriterError,
|
||||||
)
|
)
|
||||||
@@ -345,8 +347,17 @@ END:VCARD
|
|||||||
self.assertEqual(len(lookup), 1)
|
self.assertEqual(len(lookup), 1)
|
||||||
self.assertEqual(lookup[0].email, "ada@example.local")
|
self.assertEqual(lookup[0].email, "ada@example.local")
|
||||||
self.assertEqual(lookup[0].contact_id, contact.id)
|
self.assertEqual(lookup[0].contact_id, contact.id)
|
||||||
|
people_provider = AddressesPeopleSearchProvider()
|
||||||
|
self.assertIsInstance(people_provider, PeopleSearchProvider)
|
||||||
|
people = people_provider.search_people(self.session, self.principal, query="ada")
|
||||||
|
self.assertEqual(people[0].key, "contacts")
|
||||||
|
self.assertEqual(people[0].candidates[0].reference_id, contact.id)
|
||||||
|
self.assertEqual(people[0].candidates[0].source_ref, f"addresses:contact:{contact.id}")
|
||||||
|
self.assertEqual(people[0].candidates[0].metadata["address_book_id"], book.id)
|
||||||
warm_lookup = AddressesLookupCapability().lookup(self.session, self.principal, query="", limit=10)
|
warm_lookup = AddressesLookupCapability().lookup(self.session, self.principal, query="", limit=10)
|
||||||
self.assertEqual([item.email for item in warm_lookup], ["ada@example.local"])
|
self.assertEqual([item.email for item in warm_lookup], ["ada@example.local"])
|
||||||
|
self.assertIn(CAPABILITY_ADDRESSES_PEOPLE_SEARCH, manifest.capability_factories)
|
||||||
|
self.assertIn(CAPABILITY_ADDRESSES_PEOPLE_SEARCH, {item.name for item in manifest.provides_interfaces})
|
||||||
|
|
||||||
recipient_sources = AddressesRecipientSourceCapability().list_sources(self.session, self.principal)
|
recipient_sources = AddressesRecipientSourceCapability().list_sources(self.session, self.principal)
|
||||||
self.assertEqual(len(recipient_sources), 1)
|
self.assertEqual(len(recipient_sources), 1)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/addresses-webui",
|
"name": "@govoplan/addresses-webui",
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"test:ui-structure": "node scripts/test-selection-list-structure.mjs"
|
"test:ui-structure": "node scripts/test-selection-list-structure.mjs"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.9",
|
"@govoplan/core-webui": "^0.1.11",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const translations = {
|
|||||||
export const addressesModule: PlatformWebModule = {
|
export const addressesModule: PlatformWebModule = {
|
||||||
id: "addresses",
|
id: "addresses",
|
||||||
label: "i18n:govoplan-addresses.address_book.f6327f59",
|
label: "i18n:govoplan-addresses.address_book.f6327f59",
|
||||||
version: "1.0.0",
|
version: "0.1.9",
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
optionalDependencies: ["campaigns", "mail", "forms", "reporting", "portal", "postbox"],
|
optionalDependencies: ["campaigns", "mail", "forms", "reporting", "portal", "postbox"],
|
||||||
translations,
|
translations,
|
||||||
|
|||||||
Reference in New Issue
Block a user