senaite /
senaite.health
| 1 | # -*- coding: utf-8 -*- |
||
| 2 | # |
||
| 3 | # This file is part of SENAITE.HEALTH. |
||
| 4 | # |
||
| 5 | # SENAITE.HEALTH is free software: you can redistribute it and/or modify it |
||
| 6 | # under the terms of the GNU General Public License as published by the Free |
||
| 7 | # Software Foundation, version 2. |
||
| 8 | # |
||
| 9 | # This program is distributed in the hope that it will be useful, but WITHOUT |
||
| 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
||
| 11 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
||
| 12 | # details. |
||
| 13 | # |
||
| 14 | # You should have received a copy of the GNU General Public License along with |
||
| 15 | # this program; if not, write to the Free Software Foundation, Inc., 51 |
||
| 16 | # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
||
| 17 | # |
||
| 18 | # Copyright 2018-2019 by it's authors. |
||
| 19 | # Some rights reserved, see README and LICENSE. |
||
| 20 | |||
| 21 | import itertools |
||
| 22 | |||
| 23 | from Acquisition import aq_base |
||
| 24 | from Products.CMFCore import permissions |
||
| 25 | from Products.CMFCore.permissions import AccessContentsInformation |
||
| 26 | from Products.CMFCore.permissions import ListFolderContents |
||
| 27 | from Products.CMFCore.permissions import View |
||
| 28 | from Products.CMFCore.utils import getToolByName |
||
| 29 | from plone import api as ploneapi |
||
| 30 | |||
| 31 | from bika.health import bikaMessageFactory as _ |
||
| 32 | from bika.health import logger |
||
| 33 | from bika.health.catalog import \ |
||
| 34 | getCatalogDefinitions as getCatalogDefinitionsHealth |
||
| 35 | from bika.health.catalog import getCatalogExtensions |
||
| 36 | from bika.health.config import DEFAULT_PROFILE_ID |
||
| 37 | from bika.health.permissions import ViewPatients |
||
| 38 | from bika.lims import api |
||
| 39 | from bika.lims.catalog import \ |
||
| 40 | getCatalogDefinitions as getCatalogDefinitionsLIMS |
||
| 41 | from bika.lims.catalog import setup_catalogs |
||
| 42 | from bika.lims.idserver import renameAfterCreation |
||
| 43 | from bika.lims.permissions import AddAnalysisRequest |
||
| 44 | from bika.lims.permissions import AddBatch |
||
| 45 | from bika.lims.utils import tmpID |
||
| 46 | |||
| 47 | |||
| 48 | class Empty(object): |
||
| 49 | pass |
||
| 50 | |||
| 51 | |||
| 52 | ROLES = [ |
||
| 53 | # Tuple of (role, permissions) |
||
| 54 | # NOTE: here we only expect permissions that belong to other products and |
||
| 55 | # plone, cause health-specific permissions for this role are |
||
| 56 | # explicitly set in profile/rolemap.xml |
||
| 57 | ("Doctor", [View, AccessContentsInformation, ListFolderContents]), |
||
| 58 | ("Client", [AddBatch]) |
||
| 59 | ] |
||
| 60 | |||
| 61 | GROUPS = [ |
||
| 62 | # Tuple of (group_name, roles_group |
||
| 63 | ("Doctors", ["Member", "Doctor"], ), |
||
| 64 | ] |
||
| 65 | |||
| 66 | ID_FORMATTING = [ |
||
| 67 | { |
||
| 68 | # P000001, P000002 |
||
| 69 | "portal_type": "Patient", |
||
| 70 | "form": "P{seq:06d}", |
||
| 71 | "prefix": "patient", |
||
| 72 | "sequence_type": "generated", |
||
| 73 | "counter_type": "", |
||
| 74 | "split_length": 1, |
||
| 75 | }, { |
||
| 76 | # D0001, D0002, D0003 |
||
| 77 | "portal_type": "Doctor", |
||
| 78 | "form": "D{seq:04d}", |
||
| 79 | "prefix": "doctor", |
||
| 80 | "sequence_type": "generated", |
||
| 81 | "counter_type": "", |
||
| 82 | "split_length": 1, |
||
| 83 | } |
||
| 84 | ] |
||
| 85 | |||
| 86 | |||
| 87 | def post_install(portal_setup): |
||
| 88 | """Runs after the last import step of the *default* profile |
||
| 89 | This handler is registered as a *post_handler* in the generic setup profile |
||
| 90 | :param portal_setup: SetupTool |
||
| 91 | """ |
||
| 92 | logger.info("SENAITE Health post-install handler [BEGIN]") |
||
| 93 | context = portal_setup._getImportContext(DEFAULT_PROFILE_ID) |
||
| 94 | portal = context.getSite() |
||
| 95 | # Setup catalogs |
||
| 96 | # TODO use upgrade.utils.setup_catalogs instead! |
||
| 97 | setup_health_catalogs(portal) |
||
| 98 | |||
| 99 | # Setup portal permissions |
||
| 100 | setup_roles_permissions(portal) |
||
| 101 | |||
| 102 | # Setup user groups (e.g. Doctors) |
||
| 103 | setup_user_groups(portal) |
||
| 104 | |||
| 105 | # Setup site structure |
||
| 106 | setup_site_structure(context) |
||
| 107 | |||
| 108 | # Setup javascripts |
||
| 109 | setup_javascripts(portal) |
||
| 110 | |||
| 111 | # Setup content actions |
||
| 112 | setup_content_actions(portal) |
||
| 113 | |||
| 114 | # Setup ID formatting for Health types |
||
| 115 | setup_id_formatting(portal) |
||
| 116 | |||
| 117 | # Setup default ethnicities |
||
| 118 | setup_ethnicities(portal) |
||
| 119 | |||
| 120 | # Allow patients inside clients |
||
| 121 | # Note: this should always be run if core's typestool is reimported |
||
| 122 | allow_patients_inside_clients(portal) |
||
| 123 | |||
| 124 | # Reindex the top level folder in the portal and setup to fix missing icons |
||
| 125 | reindex_content_structure(portal) |
||
| 126 | |||
| 127 | # When installing senaite health together with core, health's skins are not |
||
| 128 | # set before core's, even if after-before is set in profiles/skins.xml |
||
| 129 | # Ensure health's skin layer(s) always gets priority over core's |
||
| 130 | portal_setup.runImportStepFromProfile(DEFAULT_PROFILE_ID, "skins") |
||
| 131 | |||
| 132 | # Setup default email body and subject for panic alerts |
||
| 133 | setup_panic_alerts(portal) |
||
| 134 | |||
| 135 | logger.info("SENAITE Health post-install handler [DONE]") |
||
| 136 | |||
| 137 | |||
| 138 | def setup_user_groups(portal): |
||
| 139 | logger.info("Setup User Groups ...") |
||
| 140 | portal_groups = portal.portal_groups |
||
| 141 | for group_name, roles in GROUPS: |
||
| 142 | if group_name not in portal_groups.listGroupIds(): |
||
| 143 | portal_groups.addGroup(group_name, title=group_name, roles=roles) |
||
| 144 | logger.info("Group '{}' with roles '{}' added" |
||
| 145 | .format(group_name, ", ".join(roles))) |
||
| 146 | else: |
||
| 147 | logger.info("Group '{}' already exist [SKIP]".format(group_name)) |
||
| 148 | logger.info("Setup User Groups [DONE]") |
||
| 149 | |||
| 150 | |||
| 151 | def setup_roles_permissions(portal): |
||
| 152 | """Setup the top-level permissions for new roles. The new role is added to |
||
| 153 | the roles that already have the permission granted (acquire=1) |
||
| 154 | """ |
||
| 155 | logger.info("Setup roles permissions ...") |
||
| 156 | for role_name, permissions in ROLES: |
||
| 157 | for permission in permissions: |
||
| 158 | add_permission_for_role(portal, permission, role_name) |
||
| 159 | |||
| 160 | # Add "Add AnalysisRequest" permission for Clients in base analysisrequests |
||
| 161 | # This makes the "Add" button to appear in AnalysisRequestsFolder view |
||
| 162 | analysis_requests = portal.analysisrequests |
||
| 163 | add_permission_for_role(analysis_requests, AddAnalysisRequest, "Client") |
||
| 164 | logger.info("Setup roles permissions [DONE]") |
||
| 165 | |||
| 166 | |||
| 167 | def add_permission_for_role(folder, permission, role): |
||
| 168 | """Grants a permission to the given role and given folder |
||
| 169 | :param folder: the folder to which the permission for the role must apply |
||
| 170 | :param permission: the permission to be assigned |
||
| 171 | :param role: role to which the permission must be granted |
||
| 172 | :return True if succeed, otherwise, False |
||
| 173 | """ |
||
| 174 | roles = filter(lambda perm: perm.get('selected') == 'SELECTED', |
||
| 175 | folder.rolesOfPermission(permission)) |
||
| 176 | roles = map(lambda perm_role: perm_role['name'], roles) |
||
| 177 | if role in roles: |
||
| 178 | # Nothing to do, the role has the permission granted already |
||
| 179 | logger.info( |
||
| 180 | "Role '{}' has permission {} for {} already".format(role, |
||
| 181 | repr(permission), |
||
| 182 | repr(folder))) |
||
| 183 | return False |
||
| 184 | roles.append(role) |
||
| 185 | acquire = folder.acquiredRolesAreUsedBy(permission) == 'CHECKED' and 1 or 0 |
||
| 186 | folder.manage_permission(permission, roles=roles, acquire=acquire) |
||
| 187 | folder.reindexObject() |
||
| 188 | logger.info( |
||
| 189 | "Added permission {} to role '{}' for {}".format(repr(permission), role, |
||
| 190 | repr(folder))) |
||
| 191 | return True |
||
| 192 | |||
| 193 | |||
| 194 | def setup_ethnicities(portal): |
||
| 195 | """ |
||
| 196 | Creates standard ethnicities |
||
| 197 | """ |
||
| 198 | logger.info("Setup default ethnicities ...") |
||
| 199 | ethnicities = ['Native American', 'Asian', 'Black', |
||
| 200 | 'Native Hawaiian or Other Pacific Islander', 'White', |
||
| 201 | 'Hispanic or Latino'] |
||
| 202 | folder = portal.bika_setup.bika_ethnicities |
||
| 203 | for ethnicityName in ethnicities: |
||
| 204 | _id = folder.invokeFactory('Ethnicity', id=tmpID()) |
||
| 205 | obj = folder[_id] |
||
| 206 | obj.edit(title=ethnicityName, |
||
| 207 | description='') |
||
| 208 | obj.unmarkCreationFlag() |
||
| 209 | renameAfterCreation(obj) |
||
| 210 | logger.info("Setup default ethnicities [DONE]") |
||
| 211 | |||
| 212 | |||
| 213 | def setup_site_structure(context): |
||
| 214 | """ Setup contents structure for health |
||
| 215 | """ |
||
| 216 | if context.readDataFile('bika.health.txt') is None: |
||
| 217 | return |
||
| 218 | portal = context.getSite() |
||
| 219 | logger.info("Setup site structure ...") |
||
| 220 | |||
| 221 | # index objects - importing through GenericSetup doesn't |
||
| 222 | for obj_id in ('doctors', |
||
| 223 | 'patients', ): |
||
| 224 | obj = portal._getOb(obj_id) |
||
| 225 | obj.unmarkCreationFlag() |
||
| 226 | obj.reindexObject() |
||
| 227 | |||
| 228 | # same for objects in bika_setup |
||
| 229 | bika_setup = portal.bika_setup |
||
| 230 | for obj_id in ('bika_aetiologicagents', |
||
| 231 | 'bika_analysiscategories', |
||
| 232 | 'bika_drugs', |
||
| 233 | 'bika_drugprohibitions', |
||
| 234 | 'bika_diseases', |
||
| 235 | 'bika_treatments', |
||
| 236 | 'bika_immunizations', |
||
| 237 | 'bika_vaccinationcenters', |
||
| 238 | 'bika_casestatuses', |
||
| 239 | 'bika_caseoutcomes', |
||
| 240 | 'bika_identifiertypes', |
||
| 241 | 'bika_casesyndromicclassifications', |
||
| 242 | 'bika_insurancecompanies', |
||
| 243 | 'bika_ethnicities',): |
||
| 244 | obj = bika_setup._getOb(obj_id) |
||
| 245 | obj.unmarkCreationFlag() |
||
| 246 | obj.reindexObject() |
||
| 247 | |||
| 248 | logger.info("Setup site structure [DONE]") |
||
| 249 | |||
| 250 | |||
| 251 | def setup_javascripts(portal): |
||
| 252 | # Plone's jQuery gets clobbered when jsregistry is loaded. |
||
| 253 | setup = portal.portal_setup |
||
| 254 | setup.runImportStepFromProfile('profile-plone.app.jquery:default', |
||
| 255 | 'jsregistry') |
||
| 256 | setup.runImportStepFromProfile('profile-plone.app.jquerytools:default', |
||
| 257 | 'jsregistry') |
||
| 258 | |||
| 259 | # Load bika.lims js always before bika.health ones. |
||
| 260 | setup.runImportStepFromProfile('profile-bika.lims:default', 'jsregistry') |
||
| 261 | |||
| 262 | |||
| 263 | def setup_content_actions(portal): |
||
| 264 | """Add "patients" and "doctors" action views inside Client view |
||
| 265 | """ |
||
| 266 | logger.info("Setup content actions ...") |
||
| 267 | client_type = portal.portal_types.getTypeInfo("Client") |
||
| 268 | |||
| 269 | remove_action(client_type, "patients") |
||
| 270 | client_type.addAction( |
||
| 271 | id="patients", |
||
| 272 | name="Patients", |
||
| 273 | action="string:${object_url}/patients", |
||
| 274 | permission=ViewPatients, |
||
| 275 | category="object", |
||
| 276 | visible=True, |
||
| 277 | icon_expr="string:${portal_url}/images/patient.png", |
||
| 278 | link_target="", |
||
| 279 | description="", |
||
| 280 | condition="") |
||
| 281 | |||
| 282 | remove_action(client_type, "doctors") |
||
| 283 | client_type.addAction( |
||
| 284 | id="doctors", |
||
| 285 | name="Doctors", |
||
| 286 | action="string:${object_url}/doctors", |
||
| 287 | permission=permissions.View, |
||
| 288 | category="object", |
||
| 289 | visible=True, |
||
| 290 | icon_expr="string:${portal_url}/images/doctor.png", |
||
| 291 | link_target="", |
||
| 292 | description="", |
||
| 293 | condition="") |
||
| 294 | logger.info("Setup content actions [DONE]") |
||
| 295 | |||
| 296 | |||
| 297 | def remove_action(type_info, action_id): |
||
| 298 | """Removes the action id from the type passed in |
||
| 299 | """ |
||
| 300 | actions = map(lambda action: action.id, type_info._actions) |
||
| 301 | if action_id not in actions: |
||
| 302 | return True |
||
| 303 | index = actions.index(action_id) |
||
| 304 | type_info.deleteActions([index]) |
||
| 305 | return remove_action(type_info, action_id) |
||
| 306 | |||
| 307 | |||
| 308 | def setup_health_catalogs(portal): |
||
| 309 | # an item should belong to only one catalog. |
||
| 310 | # that way looking it up means first looking up *the* catalog |
||
| 311 | # in which it is indexed, as well as making it cheaper to index. |
||
| 312 | def addIndex(cat, *args): |
||
| 313 | try: |
||
| 314 | cat.addIndex(*args) |
||
| 315 | except: |
||
| 316 | pass |
||
| 317 | |||
| 318 | def addColumn(cat, col): |
||
| 319 | try: |
||
| 320 | cat.addColumn(col) |
||
| 321 | except: |
||
| 322 | pass |
||
| 323 | |||
| 324 | logger.info("Setup catalogs ...") |
||
| 325 | |||
| 326 | # bika_catalog |
||
| 327 | bc = getToolByName(portal, 'bika_catalog', None) |
||
| 328 | if not bc: |
||
| 329 | logger.warning('Could not find the bika_catalog tool.') |
||
| 330 | return |
||
| 331 | addIndex(bc, 'getClientTitle', 'FieldIndex') |
||
| 332 | addIndex(bc, 'getPatientID', 'FieldIndex') |
||
| 333 | addIndex(bc, 'getPatientUID', 'FieldIndex') |
||
| 334 | addIndex(bc, 'getPatientTitle', 'FieldIndex') |
||
| 335 | addIndex(bc, 'getDoctorID', 'FieldIndex') |
||
| 336 | addIndex(bc, 'getDoctorUID', 'FieldIndex') |
||
| 337 | addIndex(bc, 'getDoctorTitle', 'FieldIndex') |
||
| 338 | addIndex(bc, 'getClientPatientID', 'FieldIndex') |
||
| 339 | addColumn(bc, 'getClientTitle') |
||
| 340 | addColumn(bc, 'getPatientID') |
||
| 341 | addColumn(bc, 'getPatientUID') |
||
| 342 | addColumn(bc, 'getPatientTitle') |
||
| 343 | addColumn(bc, 'getDoctorID') |
||
| 344 | addColumn(bc, 'getDoctorUID') |
||
| 345 | addColumn(bc, 'getDoctorTitle') |
||
| 346 | addColumn(bc, 'getClientPatientID') |
||
| 347 | |||
| 348 | # portal_catalog |
||
| 349 | pc = getToolByName(portal, 'portal_catalog', None) |
||
| 350 | if not pc: |
||
| 351 | logger.warning('Could not find the portal_catalog tool.') |
||
| 352 | return |
||
| 353 | addIndex(pc, 'getDoctorID', 'FieldIndex') |
||
| 354 | addIndex(pc, 'getDoctorUID', 'FieldIndex') |
||
| 355 | addIndex(pc, 'getPrimaryReferrerUID', 'FieldIndex') |
||
| 356 | addColumn(pc, 'getDoctorID') |
||
| 357 | addColumn(pc, 'getDoctorUID') |
||
| 358 | |||
| 359 | # bika_setup_catalog |
||
| 360 | bsc = getToolByName(portal, 'bika_setup_catalog', None) |
||
| 361 | if not bsc: |
||
| 362 | logger.warning('Could not find the bika_setup_catalog tool.') |
||
| 363 | return |
||
| 364 | at = getToolByName(portal, 'archetype_tool') |
||
| 365 | at.setCatalogsByType('Disease', ['bika_setup_catalog', ]) |
||
| 366 | at.setCatalogsByType('AetiologicAgent', ['bika_setup_catalog', ]) |
||
| 367 | at.setCatalogsByType('Treatment', ['bika_setup_catalog']) |
||
| 368 | at.setCatalogsByType('Symptom', ['bika_setup_catalog']) |
||
| 369 | at.setCatalogsByType('Drug', ['bika_setup_catalog']) |
||
| 370 | at.setCatalogsByType('DrugProhibition', ['bika_setup_catalog']) |
||
| 371 | at.setCatalogsByType('VaccinationCenter', ['bika_setup_catalog', ]) |
||
| 372 | at.setCatalogsByType('InsuranceCompany', ['bika_setup_catalog', ]) |
||
| 373 | at.setCatalogsByType('Immunization', ['bika_setup_catalog', ]) |
||
| 374 | at.setCatalogsByType('CaseStatus', ['bika_setup_catalog', ]) |
||
| 375 | at.setCatalogsByType('CaseOutcome', ['bika_setup_catalog', ]) |
||
| 376 | at.setCatalogsByType('IdentifierType', ['bika_setup_catalog', ]) |
||
| 377 | at.setCatalogsByType('CaseSyndromicClassification', ['bika_setup_catalog']) |
||
| 378 | at.setCatalogsByType('Ethnicity', ['bika_setup_catalog', ]) |
||
| 379 | |||
| 380 | addIndex(bsc, 'getGender', 'FieldIndex') |
||
| 381 | addColumn(bsc, 'getGender') |
||
| 382 | |||
| 383 | catalog_definitions_lims_health = getCatalogDefinitionsLIMS() |
||
| 384 | catalog_definitions_lims_health.update(getCatalogDefinitionsHealth()) |
||
| 385 | # Updating health catalogs if there is any change in them |
||
| 386 | setup_catalogs( |
||
| 387 | portal, catalog_definitions_lims_health, |
||
| 388 | catalogs_extension=getCatalogExtensions()) |
||
| 389 | |||
| 390 | logger.info("Setup catalogs [DONE]") |
||
| 391 | |||
| 392 | |||
| 393 | def setup_id_formatting(portal, format=None): |
||
| 394 | """Setup default ID Formatting for health content types |
||
| 395 | """ |
||
| 396 | if not format: |
||
| 397 | logger.info("Setup ID formatting ...") |
||
| 398 | for formatting in ID_FORMATTING: |
||
| 399 | setup_id_formatting(portal, format=formatting) |
||
| 400 | logger.info("Setup ID formatting [DONE]") |
||
| 401 | return |
||
| 402 | |||
| 403 | bs = portal.bika_setup |
||
| 404 | p_type = format.get("portal_type", None) |
||
| 405 | if not p_type: |
||
| 406 | return |
||
| 407 | id_map = bs.getIDFormatting() |
||
| 408 | id_format = filter(lambda idf: idf.get("portal_type", "") == p_type, id_map) |
||
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
| 409 | if id_format: |
||
| 410 | logger.info("ID Format for {} already set: '{}' [SKIP]" |
||
| 411 | .format(p_type, id_format[0]["form"])) |
||
| 412 | return |
||
| 413 | |||
| 414 | form = format.get("form", "") |
||
| 415 | if not form: |
||
| 416 | logger.info("Param 'form' for portal type {} not set [SKIP") |
||
| 417 | return |
||
| 418 | |||
| 419 | logger.info("Applying format '{}' for {}".format(form, p_type)) |
||
| 420 | ids = list() |
||
| 421 | for record in id_map: |
||
| 422 | if record.get('portal_type', '') == p_type: |
||
| 423 | continue |
||
| 424 | ids.append(record) |
||
| 425 | ids.append(format) |
||
| 426 | bs.setIDFormatting(ids) |
||
| 427 | |||
| 428 | |||
| 429 | def reindex_content_structure(portal): |
||
| 430 | """Reindex contents generated by Generic Setup |
||
| 431 | """ |
||
| 432 | logger.info("*** Reindex content structure ***") |
||
| 433 | |||
| 434 | def reindex(obj, recurse=False): |
||
| 435 | # skip catalog tools etc. |
||
| 436 | if api.is_object(obj): |
||
| 437 | obj.reindexObject() |
||
| 438 | if recurse and hasattr(aq_base(obj), "objectValues"): |
||
| 439 | map(reindex, obj.objectValues()) |
||
| 440 | |||
| 441 | setup = api.get_setup() |
||
| 442 | setupitems = setup.objectValues() |
||
| 443 | rootitems = portal.objectValues() |
||
| 444 | |||
| 445 | for obj in itertools.chain(setupitems, rootitems): |
||
| 446 | if not api.is_object(obj): |
||
| 447 | continue |
||
| 448 | logger.info("Reindexing {}".format(repr(obj))) |
||
| 449 | reindex(obj) |
||
| 450 | |||
| 451 | |||
| 452 | def setup_panic_alerts(portal): |
||
| 453 | """Setups the template texts for panic alert email's subject and body |
||
| 454 | """ |
||
| 455 | email_body = _( |
||
| 456 | "Some results from the Sample ${sample_id} exceeded the panic levels " |
||
| 457 | "that may indicate an imminent life-threatening condition:\n\n" |
||
| 458 | "${analyses}\n\n--\n${lab_address}") |
||
| 459 | ploneapi.portal.set_registry_record("senaite.panic.email_body", email_body) |
||
| 460 | |||
| 461 | |||
| 462 | def allow_patients_inside_clients(portal): |
||
| 463 | """Allows Patient content type to be created inside Client |
||
| 464 | """ |
||
| 465 | portal_types = api.get_tool('portal_types') |
||
| 466 | client = getattr(portal_types, 'Client') |
||
| 467 | allowed_types = client.allowed_content_types |
||
| 468 | if 'Patient' not in allowed_types: |
||
| 469 | client.allowed_content_types = allowed_types + ('Patient', ) |
||
| 470 |