bika.health.setuphandlers.setup_id_formatting()   C
last analyzed

Complexity

Conditions 9

Size

Total Lines 34
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 29
dl 0
loc 34
rs 6.6666
c 0
b 0
f 0
cc 9
nop 2
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
The variable p_type does not seem to be defined for all execution paths.
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