Skip to content
Open
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 docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
1.6.0 (unreleased)
------------------

- #118 Remove patient-specific settings, catalogs and objects on uninstall
- #131 Fix BehaviorRegistrationNotFound on Patient creation via JSON API
- #130 Fix cannot search samples by MRN or patient name
- #127 Add adapter for patient add views
Expand Down
12 changes: 3 additions & 9 deletions src/senaite/patient/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
<include package=".subscribers" />
<include package=".upgrade" />

<!-- File includes -->
<include file="uninstall.zcml"/>

<!-- Default profile -->
<genericsetup:registerProfile
name="default"
Expand All @@ -89,13 +92,4 @@
<depends name="workflow"/>
</genericsetup:importStep>

<!-- Uninstall profile -->
<genericsetup:registerProfile
name="uninstall"
title="SENAITE PATIENT (uninstall)"
directory="profiles/uninstall"
description="Uninstalls SENAITE PATIENT"
post_handler=".setuphandlers.post_uninstall"
provides="Products.GenericSetup.interfaces.EXTENSION" />

</configure>
8 changes: 1 addition & 7 deletions src/senaite/patient/content/patient.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
# Copyright 2020-2025 by it's authors.
# Some rights reserved, see README and LICENSE.

from datetime import datetime
from string import Template

from AccessControl import ClassSecurityInfo
from bika.lims import api
from bika.lims.api.mail import is_valid_email_address
from datetime import datetime
from plone.autoform import directives
from plone.supermodel import model
from plone.supermodel.directives import fieldset
Expand All @@ -44,7 +44,6 @@
from senaite.core.z3cform.widgets.phone import PhoneWidgetFactory
from senaite.patient import api as patient_api
from senaite.patient import messageFactory as _
from senaite.patient.catalog import PATIENT_CATALOG
from senaite.patient.config import GENDERS
from senaite.patient.config import SEXES
from senaite.patient.i18n import translate
Expand Down Expand Up @@ -459,11 +458,6 @@ def validate_age(data):
class Patient(Container):
"""Results Interpretation Template content
"""

# XXX Remove after 1.5.0
# See https://github.com/senaite/senaite.patient/pull/119
_catalogs = [PATIENT_CATALOG]

security = ClassSecurityInfo()

@security.protected(permissions.View)
Expand Down
19 changes: 0 additions & 19 deletions src/senaite/patient/setuphandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@

PROFILE_ID = "profile-{}:default".format(PRODUCT_NAME)

# Maximum threshold in seconds before a transaction.commit takes place
# Default: 300 (5 minutes)
MAX_SEC_THRESHOLD = 300

CATALOGS = (
PatientCatalog,
)
Expand Down Expand Up @@ -218,21 +214,6 @@ def post_install(portal_setup):
logger.info("{} install handler [DONE]".format(PRODUCT_NAME.upper()))


def post_uninstall(portal_setup):
"""Runs after the last import step of the *uninstall* profile
This handler is registered as a *post_handler* in the generic setup profile
:param portal_setup: SetupTool
"""
logger.info("{} uninstall handler [BEGIN]".format(PRODUCT_NAME.upper()))

# https://docs.plone.org/develop/addons/components/genericsetup.html#custom-installer-code-setuphandlers-py
profile_id = "profile-{}:uninstall".format(PRODUCT_NAME)
context = portal_setup._getImportContext(profile_id) # noqa
portal = context.getSite() # noqa

logger.info("{} uninstall handler [DONE]".format(PRODUCT_NAME.upper()))


def setup_catalogs(portal):
"""Setup patient catalogs
"""
Expand Down
153 changes: 153 additions & 0 deletions src/senaite/patient/uninstall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-

from bika.lims import api
from bika.lims.api import delete
from plone.registry.interfaces import IRegistry
from Products.GenericSetup.utils import _resolveDottedName
from senaite.core.catalog import set_catalogs
from senaite.patient import logger
from senaite.patient import PRODUCT_NAME
from senaite.patient.setuphandlers import CATALOG_MAPPINGS
from senaite.patient.setuphandlers import CATALOGS
from senaite.patient.setuphandlers import COLUMNS
from senaite.patient.setuphandlers import INDEXES
from zope.component import getUtility

PROFILE_ID = "profile-{}:uninstall".format(PRODUCT_NAME)

TYPES_TO_REMOVE = ["Patient", "PatientFolder"]


def pre_uninstall(portal_setup):
"""Runs before the first import step of the *uninstall* profile
This handler is registered as a *pre_handler* in the generic setup profile
:param portal_setup: SetupTool
"""
logger.info("%s pre-uninstall handler [BEGIN]" % PRODUCT_NAME.upper())

context = portal_setup._getImportContext(PROFILE_ID) # noqa
portal = context.getSite() # noqa

# Purge product-specific types from navigation bar
purge_navigation_types(portal)

# Purge product-specific objects
purge_objects(portal)

# Purge product-specific catalogs, indexes and metadata
purge_catalogs(portal)

# Purge product-specific records from ID formatting
purge_id_formatting(portal)

logger.info("%s pre-uninstall handler [DONE]" % PRODUCT_NAME.upper())


def post_uninstall(portal_setup):
"""Runs after the last import step of the *uninstall* profile
This handler is registered as a *post_handler* in the generic setup profile
:param portal_setup: SetupTool
"""
logger.info("%s post-uninstall handler [BEGIN]" % PRODUCT_NAME.upper())

context = portal_setup._getImportContext(PROFILE_ID) # noqa
portal = context.getSite() # noqa

logger.info("%s post-uninstall handler [DONE]" % PRODUCT_NAME.upper())


def purge_objects(portal):
"""Deletes objects that are specific of this product
"""
logger.info("Purge objects ...")

# Delete patients folder and objects inside
patients = portal.get("patients")
if patients:
logger.info("Removing folder: %s" % api.get_path(patients))
delete(patients, check_permissions=False)

# Delete patient objects left elsewhere (e.g in client folders)
uc = api.get_tool("uid_catalog")
brains = list(uc(portal_type=TYPES_TO_REMOVE))
for brain in brains:
try:
obj = brain.getObject()
except AttributeError:
# Object does no longer exist, un-catalog the brain
path = api.get_path(brain)
# For DXs, uids of uid_catalog are absolute paths to portal root
# see plone.app.referencablebehavior.uidcatalog
logger.info("Removing stale brain: %s" % path)
uc.uncatalog_object(path)
continue

# Delete the patient object
path = api.get_path(obj)
logger.info("Removing patient: %s" % path)
delete(obj, check_permissions=False)

logger.info("Purge objects [DONE]")


def purge_catalogs(portal):
"""Uninstall catalogs, indexes and columns that are product-specific
"""
logger.info("Purge catalogs ...")

# Delete product-specific indexes
for index_info in INDEXES:
cat_id = index_info[0]
idx_name = index_info[1]
cat = api.get_tool(cat_id)
if idx_name in cat.indexes():
logger.info("Removing index from %s: %s" % (cat.id, idx_name))
cat.delIndex(idx_name)

# Delete product-specific columns
for cat_id, column in COLUMNS:
cat = api.get_tool(cat_id)
if column not in cat.schema():
logger.info("Removing column from %s: %s" % (cat.id, column))
cat.delColumn(column)

# Delete product-specific catalogs
for clazz in CATALOGS:
module = _resolveDottedName(clazz.__module__)
catalog_id = module.CATALOG_ID
logger.info("Removing catalog: %s" % catalog_id)
portal.manage_delObjects([catalog_id])

# Clear catalog mappings
for portal_type, catalogs in CATALOG_MAPPINGS:
logger.info("Flushing catalog mappings for %s" % portal_type)
set_catalogs(portal_type, tuple())

logger.info("Purge catalogs [DONE]")


def purge_id_formatting(portal):
"""Purges ID formatting records that are specific of this product
"""
logger.info("Purge ID formatting ...")

ids = ["Patient", "MedicalRecordNumber"]
records = portal.bika_setup.getIDFormatting()
records = filter(lambda rec: rec.get("portal_type") not in ids, records)
portal.bika_setup.setIDFormatting(records)

logger.info("Purge ID formatting [DONE]")


def purge_navigation_types(portal):
"""Purges product-specific types from the navigation menu
"""
logger.info("Purge navigation types ...")

key = "plone.displayed_types"
registry = getUtility(IRegistry)
to_display = registry.get(key, ())
to_display = filter(lambda ty: ty not in TYPES_TO_REMOVE, to_display)
registry[key] = tuple(to_display)

logger.info("Purge navigation types [DONE]")
15 changes: 15 additions & 0 deletions src/senaite/patient/uninstall.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup">

<!-- Uninstall profile -->
<genericsetup:registerProfile
name="uninstall"
title="SENAITE PATIENT (uninstall)"
directory="profiles/uninstall"
description="Uninstalls SENAITE PATIENT"
pre_handler=".uninstall.pre_uninstall"
post_handler=".uninstall.post_uninstall"
provides="Products.GenericSetup.interfaces.EXTENSION"/>

</configure>