Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support DX type catalogs lookup #2408

Merged
merged 5 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
2.5.0 (unreleased)
------------------

- #2408 Support DX type catalogs lookup
- #2407 Fix analyses sort order in Transposed Multi Results Form
- #2406 Fix missing interim fields in Transposed Multi Results Form
- #2400 Add Transposed Multi Results Form
Expand Down
67 changes: 54 additions & 13 deletions src/bika/lims/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from plone.dexterity.schema import SchemaInvalidatedEvent
from plone.dexterity.utils import addContentToContainer
from plone.dexterity.utils import createContent
from plone.dexterity.utils import resolveDottedName
from plone.i18n.normalizer.interfaces import IFileNameNormalizer
from plone.i18n.normalizer.interfaces import IIDNormalizer
from plone.memoize.volatile import DontCache
Expand Down Expand Up @@ -372,7 +373,7 @@ def get_object(brain_object_uid, default=_marker):
:returns: The full object
"""
if is_uid(brain_object_uid):
return get_object_by_uid(brain_object_uid)
return get_object_by_uid(brain_object_uid, default=default)
elif is_supermodel(brain_object_uid):
return brain_object_uid.instance
if not is_object(brain_object_uid):
Expand Down Expand Up @@ -1080,6 +1081,19 @@ def is_active(brain_or_object):
return True


def get_fti(portal_type, default=None):
"""Lookup the Dynamic Filetype Information for the given portal_type

:param portal_type: The portal type to get the FTI for
:returns: FTI or default value
"""
if not is_string(portal_type):
return default
portal_types = get_tool("portal_types")
fti = portal_types.getTypeInfo(portal_type)
return fti or default


def get_catalogs_for(brain_or_object, default=PORTAL_CATALOG):
"""Get all registered catalogs for the given portal_type, catalog brain or
content object
Expand All @@ -1093,23 +1107,50 @@ def get_catalogs_for(brain_or_object, default=PORTAL_CATALOG):
:returns: List of supported catalogs
:rtype: list
"""
archetype_tool = get_tool("archetype_tool", default=None)
if archetype_tool is None:
# return the default catalog
return [get_tool(default, default=PORTAL_CATALOG)]

# only handle catalog lookups by portal_type internally
if is_uid(brain_or_object) or is_object(brain_or_object):
obj = get_object(brain_or_object)
portal_type = get_portal_type(obj)
return get_catalogs_for(portal_type)

catalogs = []

# get the registered catalogs for portal_type
if is_object(brain_or_object):
catalogs = archetype_tool.getCatalogsByType(
get_portal_type(brain_or_object))
if isinstance(brain_or_object, six.string_types):
catalogs = archetype_tool.getCatalogsByType(brain_or_object)
if not is_string(brain_or_object):
raise APIError("Expected a portal_type string, got <%s>"
% type(brain_or_object))

# at this point the brain_or_object is a portal_type
portal_type = brain_or_object

# check static portal_type -> catalog mapping first
from senaite.core.catalog import get_catalogs_by_type
catalogs = get_catalogs_by_type(portal_type)

# no catalogs in static mapping
# => Lookup catalogs by FTI
if len(catalogs) == 0:
fti = get_fti(portal_type)
if fti.product:
# AT content type
# => Looup via archetype_tool
archetype_tool = get_tool("archetype_tool")
catalogs = archetype_tool.catalog_map.get(portal_type)
else:
# DX content type
# => resolve the `_catalogs` attribute from the class
klass = resolveDottedName(fti.klass)
# XXX: Refactor multi-catalog behavior to not rely
# on this hidden `_catalogs` attribute!
catalogs = getattr(klass, "_catalogs", [])

# fetch the catalog objects
catalogs = filter(None, map(lambda cid: get_tool(cid, None), catalogs))

if not catalogs:
if len(catalogs) == 0:
return [get_tool(default, default=PORTAL_CATALOG)]
return catalogs

return list(catalogs)


def get_transitions_for(brain_or_object):
Expand Down
72 changes: 72 additions & 0 deletions src/senaite/core/catalog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,75 @@
from senaite.core.catalog.worksheet_catalog import \
CATALOG_ID as WORKSHEET_CATALOG
from senaite.core.catalog.worksheet_catalog import WorksheetCatalog

CATALOG_MAPPINGS = (
# portal_type, catalog_ids
("ARReport", [REPORT_CATALOG]),
("ARTemplate", [SETUP_CATALOG]),
("Analysis", [ANALYSIS_CATALOG]),
("AnalysisCategory", [SETUP_CATALOG]),
("AnalysisProfile", [SETUP_CATALOG]),
("AnalysisRequest", [SAMPLE_CATALOG]),
("AnalysisService", [SETUP_CATALOG]),
("AnalysisSpec", [SETUP_CATALOG]),
("Attachment", [SENAITE_CATALOG]),
("AttachmentType", [SETUP_CATALOG]),
("AutoImportLog", [AUTOIMPORTLOG_CATALOG]),
("Batch", [SENAITE_CATALOG]),
("BatchLabel", [SETUP_CATALOG]),
("Calculation", [SETUP_CATALOG]),
("Client", [CLIENT_CATALOG]),
("Contact", [CONTACT_CATALOG]),
("Container", [SETUP_CATALOG]),
("ContainerType", [SETUP_CATALOG]),
("Department", [SETUP_CATALOG]),
("DuplicateAnalysis", [ANALYSIS_CATALOG]),
("Instrument", [SETUP_CATALOG]),
("InstrumentCalibration", [SETUP_CATALOG]),
("InstrumentCertification", [SETUP_CATALOG]),
("InstrumentLocation", [SETUP_CATALOG]),
("InstrumentMaintenanceTask", [SETUP_CATALOG]),
("InstrumentScheduledTask", [SETUP_CATALOG]),
("InstrumentType", [SETUP_CATALOG]),
("InstrumentValidation", [SETUP_CATALOG]),
("Invoice", [SENAITE_CATALOG]),
("LabContact", [CONTACT_CATALOG]),
("LabProduct", [SETUP_CATALOG]),
("Label", [SETUP_CATALOG]),
("Laboratory", [SETUP_CATALOG]),
("Manufacturer", [SETUP_CATALOG]),
("Method", [SETUP_CATALOG]),
("Multifile", [SETUP_CATALOG]),
("Preservation", [SETUP_CATALOG]),
("Pricelist", [SETUP_CATALOG]),
("ReferenceAnalysis", [ANALYSIS_CATALOG]),
("ReferenceDefinition", [SETUP_CATALOG]),
("ReferenceSample", [SENAITE_CATALOG]),
("RejectAnalysis", [ANALYSIS_CATALOG]),
("SampleCondition", [SETUP_CATALOG]),
("SampleMatrix", [SETUP_CATALOG]),
("SamplePoint", [SETUP_CATALOG]),
("SampleType", [SETUP_CATALOG]),
("SamplingDeviation", [SETUP_CATALOG]),
("StorageLocation", [SETUP_CATALOG]),
("SubGroup", [SETUP_CATALOG]),
("Supplier", [SETUP_CATALOG]),
("SupplierContact", [CONTACT_CATALOG]),
("Worksheet", [WORKSHEET_CATALOG]),
("WorksheetTemplate", [SETUP_CATALOG]),
)

def get_catalogs_by_type(portal_type):
"""Return the mapped catalogs by type

TODO: Provide registry setting for this mapping lookup

:param portal_type: The portal type to look up
"""
if not isinstance(portal_type, str):
raise TypeError("Expected string type, got <%s>" % type(portal_type))
mapping = dict(CATALOG_MAPPINGS)
catalogs = mapping.get(portal_type)
if not catalogs:
return []
return catalogs
67 changes: 1 addition & 66 deletions src/senaite/core/setuphandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,8 @@
from senaite.core.api.catalog import get_columns
from senaite.core.api.catalog import get_indexes
from senaite.core.api.catalog import reindex_index
from senaite.core.catalog import ANALYSIS_CATALOG
from senaite.core.catalog import AUDITLOG_CATALOG
from senaite.core.catalog import AUTOIMPORTLOG_CATALOG
from senaite.core.catalog import CLIENT_CATALOG
from senaite.core.catalog import CONTACT_CATALOG
from senaite.core.catalog import REPORT_CATALOG
from senaite.core.catalog import SAMPLE_CATALOG
from senaite.core.catalog import SENAITE_CATALOG
from senaite.core.catalog import SETUP_CATALOG
from senaite.core.catalog import WORKSHEET_CATALOG
from senaite.core.catalog import CATALOG_MAPPINGS
from senaite.core.catalog import AnalysisCatalog
from senaite.core.catalog import AuditlogCatalog
from senaite.core.catalog import AutoImportLogCatalog
Expand Down Expand Up @@ -128,63 +120,6 @@ def getNonInstallableProfiles(self):
# catalog, column name
)

CATALOG_MAPPINGS = (
# portal_type, catalog_ids
("ARReport", [REPORT_CATALOG]),
("ARTemplate", [SETUP_CATALOG]),
("Analysis", [ANALYSIS_CATALOG]),
("AnalysisCategory", [SETUP_CATALOG]),
("AnalysisProfile", [SETUP_CATALOG]),
("AnalysisRequest", [SAMPLE_CATALOG]),
("AnalysisService", [SETUP_CATALOG]),
("AnalysisSpec", [SETUP_CATALOG]),
("Attachment", [SENAITE_CATALOG]),
("AttachmentType", [SETUP_CATALOG]),
("AutoImportLog", [AUTOIMPORTLOG_CATALOG]),
("Batch", [SENAITE_CATALOG]),
("BatchLabel", [SETUP_CATALOG]),
("Calculation", [SETUP_CATALOG]),
("Client", [CLIENT_CATALOG]),
("Contact", [CONTACT_CATALOG]),
("Container", [SETUP_CATALOG]),
("ContainerType", [SETUP_CATALOG]),
("Department", [SETUP_CATALOG]),
("DuplicateAnalysis", [ANALYSIS_CATALOG]),
("Instrument", [SETUP_CATALOG]),
("InstrumentCalibration", [SETUP_CATALOG]),
("InstrumentCertification", [SETUP_CATALOG]),
("InstrumentLocation", [SETUP_CATALOG]),
("InstrumentMaintenanceTask", [SETUP_CATALOG]),
("InstrumentScheduledTask", [SETUP_CATALOG]),
("InstrumentType", [SETUP_CATALOG]),
("InstrumentValidation", [SETUP_CATALOG]),
("Invoice", [SENAITE_CATALOG]),
("LabContact", [CONTACT_CATALOG]),
("LabProduct", [SETUP_CATALOG]),
("Label", [SETUP_CATALOG]),
("Laboratory", [SETUP_CATALOG]),
("Manufacturer", [SETUP_CATALOG]),
("Method", [SETUP_CATALOG]),
("Multifile", [SETUP_CATALOG]),
("Preservation", [SETUP_CATALOG]),
("Pricelist", [SETUP_CATALOG]),
("ReferenceAnalysis", [ANALYSIS_CATALOG]),
("ReferenceDefinition", [SETUP_CATALOG]),
("ReferenceSample", [SENAITE_CATALOG]),
("RejectAnalysis", [ANALYSIS_CATALOG]),
("SampleCondition", [SETUP_CATALOG]),
("SampleMatrix", [SETUP_CATALOG]),
("SamplePoint", [SETUP_CATALOG]),
("SampleType", [SETUP_CATALOG]),
("SamplingDeviation", [SETUP_CATALOG]),
("StorageLocation", [SETUP_CATALOG]),
("SubGroup", [SETUP_CATALOG]),
("Supplier", [SETUP_CATALOG]),
("SupplierContact", [CONTACT_CATALOG]),
("Worksheet", [WORKSHEET_CATALOG]),
("WorksheetTemplate", [SETUP_CATALOG]),
)

REMOVE_PORTAL_CATALOG_INDEXES = (
"Analyst",
"SearchableText",
Expand Down
38 changes: 31 additions & 7 deletions src/senaite/core/tests/doctests/API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -744,17 +744,41 @@ Or provide a correct query::
Getting the registered Catalogs
...............................

SENAITE LIMS uses multiple catalogs registered via the Archetype Tool. This
function returns a list of registered catalogs for a brain or object::
SENAITE LIMS uses **multiple catalogs** for different content types.
This function returns a list of registered catalogs for a brain, object, UID or portal_type.

Get the mapped catalogs for an AT content type:

>>> api.get_catalogs_for(client)
[...]
[<ClientCatalog at /plone/senaite_catalog_client>]

>>> api.get_catalogs_for(instrument1)
[...]
Passing in the portal_type should return the same:

>>> api.get_catalogs_for(api.get_portal_type(client))
[<ClientCatalog at /plone/senaite_catalog_client>]

Even if we pass in the UID, we should get the same results:

>>> api.get_catalogs_for(api.get_uid(client))
[<ClientCatalog at /plone/senaite_catalog_client>]

Dexterity contents that provide IMulitCatalogBehavior should work as well:

>>> api.get_catalogs_for(senaite_setup)
[<CatalogTool at /plone/portal_catalog>]


Getting the FTI for a portal type
.................................

This function provides the dynamic type information for a given portal type:

>>> api.get_fti("Client")
<DynamicViewTypeInformation at /plone/portal_types/Client>

>>> api.get_fti("Label")
<DexterityFTI at /plone/portal_types/Label>

>>> api.get_catalogs_for(analysiscategory1)
[...]


Getting an Attribute of an Object
Expand Down