Passed
Push — master ( 651eb8...492e76 )
by Jordi
03:17
created

reindex_content_structure()   B

Complexity

Conditions 6

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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