Passed
Pull Request — 2.x (#1739)
by Jordi
05:28
created

bika.lims.setuphandlers.add_dexterity_items()   A

Complexity

Conditions 5

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 21
rs 9.2833
c 0
b 0
f 0
cc 5
nop 2
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# 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-2021 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.lims import api
25
from bika.lims import logger
26
from bika.lims.catalog import auditlog_catalog
27
from bika.lims.catalog import getCatalogDefinitions
28
from bika.lims.catalog import setup_catalogs
29
from bika.lims.catalog.catalog_utilities import addZCTextIndex
30
from plone import api as ploneapi
31
from senaite.core.upgrade.utils import temporary_allow_type
32
33
PROFILE_ID = "profile-bika.lims:default"
34
35
GROUPS = [
36
    {
37
        "id": "Analysts",
38
        "title": "Analysts",
39
        "roles": ["Analyst"],
40
    }, {
41
        "id": "Clients",
42
        "title": "Clients",
43
        "roles": ["Client"],
44
    }, {
45
        "id": "LabClerks",
46
        "title": "Lab Clerks",
47
        "roles": ["LabClerk"],
48
    }, {
49
        "id": "LabManagers",
50
        "title": "Lab Managers",
51
        "roles": ["LabManager"],
52
    }, {
53
        "id": "Preservers",
54
        "title": "Preservers",
55
        "roles": ["Preserver"],
56
    }, {
57
        "id": "Publishers",
58
        "title": "Publishers",
59
        "roles": ["Publisher"],
60
    }, {
61
        "id": "Verifiers",
62
        "title": "Verifiers",
63
        "roles": ["Verifier"],
64
    }, {
65
        "id": "Samplers",
66
        "title": "Samplers",
67
        "roles": ["Sampler"],
68
    }, {
69
        "id": "RegulatoryInspectors",
70
        "title": "Regulatory Inspectors",
71
        "roles": ["RegulatoryInspector"],
72
    }, {
73
        "id": "SamplingCoordinators",
74
        "title": "Sampling Coordinator",
75
        "roles": ["SamplingCoordinator"],
76
    }
77
]
78
79
NAV_BAR_ITEMS_TO_HIDE = (
80
    # List of items to hide from navigation bar
81
    "pricelists",
82
)
83
84
85
CONTENTS_TO_DELETE = (
86
    # List of items to delete
87
    "Members",
88
    "news",
89
    "events",
90
)
91
92
CATALOG_MAPPINGS = (
93
    # portal_type, catalog_ids
94
    ("ARTemplate", ["bika_setup_catalog", "portal_catalog"]),
95
    ("AnalysisCategory", ["bika_setup_catalog"]),
96
    ("AnalysisProfile", ["bika_setup_catalog", "portal_catalog"]),
97
    ("AnalysisService", ["bika_setup_catalog", "portal_catalog"]),
98
    ("AnalysisSpec", ["bika_setup_catalog"]),
99
    ("Attachment", ["portal_catalog"]),
100
    ("AttachmentType", ["bika_setup_catalog"]),
101
    ("Batch", ["bika_catalog", "portal_catalog"]),
102
    ("BatchLabel", ["bika_setup_catalog"]),
103
    ("Calculation", ["bika_setup_catalog", "portal_catalog"]),
104
    ("Container", ["bika_setup_catalog"]),
105
    ("ContainerType", ["bika_setup_catalog"]),
106
    ("Department", ["bika_setup_catalog", "portal_catalog"]),
107
    ("Instrument", ["bika_setup_catalog", "portal_catalog"]),
108
    ("InstrumentLocation", ["bika_setup_catalog", "portal_catalog"]),
109
    ("InstrumentType", ["bika_setup_catalog", "portal_catalog"]),
110
    ("LabContact", ["bika_setup_catalog", "portal_catalog"]),
111
    ("LabProduct", ["bika_setup_catalog", "portal_catalog"]),
112
    ("Manufacturer", ["bika_setup_catalog", "portal_catalog"]),
113
    ("Method", ["bika_setup_catalog", "portal_catalog"]),
114
    ("Multifile", ["bika_setup_catalog"]),
115
    ("Preservation", ["bika_setup_catalog"]),
116
    ("ReferenceDefinition", ["bika_setup_catalog", "portal_catalog"]),
117
    ("ReferenceSample", ["bika_catalog", "portal_catalog"]),
118
    ("SampleCondition", ["bika_setup_catalog"]),
119
    ("SampleMatrix", ["bika_setup_catalog"]),
120
    ("SamplePoint", ["bika_setup_catalog", "portal_catalog"]),
121
    ("SampleType", ["bika_setup_catalog", "portal_catalog"]),
122
    ("SamplingDeviation", ["bika_setup_catalog"]),
123
    ("StorageLocation", ["bika_setup_catalog", "portal_catalog"]),
124
    ("SubGroup", ["bika_setup_catalog"]),
125
    ("Supplier", ["bika_setup_catalog", "portal_catalog"]),
126
    ("WorksheetTemplate", ["bika_setup_catalog", "portal_catalog"]),
127
)
128
129
INDEXES = (
130
    # catalog, id, indexed attribute, type
131
    ("bika_catalog", "BatchDate", "", "DateIndex"),
132
    ("bika_catalog", "Creator", "", "FieldIndex"),
133
    ("bika_catalog", "Description", "", "ZCTextIndex"),
134
    ("bika_catalog", "Title", "", "ZCTextIndex"),
135
    ("bika_catalog", "Type", "", "FieldIndex"),
136
    ("bika_catalog", "UID", "", "FieldIndex"),
137
    ("bika_catalog", "allowedRolesAndUsers", "", "KeywordIndex"),
138
    ("bika_catalog", "created", "", "DateIndex"),
139
    ("bika_catalog", "getBlank", "", "BooleanIndex"),
140
    ("bika_catalog", "getClientBatchID", "", "FieldIndex"),
141
    ("bika_catalog", "getClientID", "", "FieldIndex"),
142
    ("bika_catalog", "getClientTitle", "", "FieldIndex"),
143
    ("bika_catalog", "getClientUID", "", "FieldIndex"),
144
    ("bika_catalog", "getDateReceived", "", "DateIndex"),
145
    ("bika_catalog", "getDateSampled", "", "DateIndex"),
146
    ("bika_catalog", "getDueDate", "", "DateIndex"),
147
    ("bika_catalog", "getExpiryDate", "", "DateIndex"),
148
    ("bika_catalog", "getId", "", "FieldIndex"),
149
    ("bika_catalog", "getReferenceDefinitionUID", "", "FieldIndex"),
150
    ("bika_catalog", "getSupportedServices", "", "KeywordIndex"),
151
    ("bika_catalog", "id", "getId", "FieldIndex"),
152
    ("bika_catalog", "isValid", "", "BooleanIndex"),
153
    ("bika_catalog", "is_active", "", "BooleanIndex"),
154
    ("bika_catalog", "path", "getPhysicalPath", "ExtendedPathIndex"),
155
    ("bika_catalog", "portal_type", "", "FieldIndex"),
156
    ("bika_catalog", "review_state", "", "FieldIndex"),
157
    ("bika_catalog", "sortable_title", "", "FieldIndex"),
158
    ("bika_catalog", "title", "", "FieldIndex"),
159
    ("bika_catalog", "listing_searchable_text", "", "TextIndexNG3"),
160
161
    ("bika_setup_catalog", "Creator", "", "FieldIndex"),
162
    ("bika_setup_catalog", "Description", "", "ZCTextIndex"),
163
    ("bika_setup_catalog", "Title", "", "ZCTextIndex"),
164
    ("bika_setup_catalog", "Type", "", "FieldIndex"),
165
    ("bika_setup_catalog", "UID", "", "FieldIndex"),
166
    ("bika_setup_catalog", "allowedRolesAndUsers", "", "KeywordIndex"),
167
    ("bika_setup_catalog", "category_uid", "", "KeywordIndex"),
168
    ("bika_setup_catalog", "created", "", "DateIndex"),
169
    ("bika_setup_catalog", "department_title", "", "KeywordIndex"),
170
    ("bika_setup_catalog", "department_uid", "", "KeywordIndex"),
171
    ("bika_setup_catalog", "department_id", "", "KeywordIndex"),
172
    ("bika_setup_catalog", "getClientUID", "", "FieldIndex"),
173
    ("bika_setup_catalog", "getId", "", "FieldIndex"),
174
    ("bika_setup_catalog", "getKeyword", "", "FieldIndex"),
175
    ("bika_setup_catalog", "id", "getId", "FieldIndex"),
176
    ("bika_setup_catalog", "instrument_title", "", "KeywordIndex"),
177
    ("bika_setup_catalog", "instrumenttype_title", "", "KeywordIndex"),
178
    ("bika_setup_catalog", "is_active", "", "BooleanIndex"),
179
    ("bika_setup_catalog", "listing_searchable_text", "", "TextIndexNG3"),
180
    ("bika_setup_catalog", "method_available_uid", "", "KeywordIndex"),
181
    ("bika_setup_catalog", "path", "getPhysicalPath", "ExtendedPathIndex"),
182
    ("bika_setup_catalog", "point_of_capture", "", "FieldIndex"),
183
    ("bika_setup_catalog", "portal_type", "", "FieldIndex"),
184
    ("bika_setup_catalog", "price", "", "FieldIndex"),
185
    ("bika_setup_catalog", "price_total", "", "FieldIndex"),
186
    ("bika_setup_catalog", "review_state", "", "FieldIndex"),
187
    ("bika_setup_catalog", "sampletype_title", "", "KeywordIndex"),
188
    ("bika_setup_catalog", "sampletype_uid", "", "KeywordIndex"),
189
    ("bika_setup_catalog", "sortable_title", "", "FieldIndex"),
190
    ("bika_setup_catalog", "title", "", "FieldIndex"),
191
192
    ("portal_catalog", "Analyst", "", "FieldIndex"),
193
    ("portal_catalog", "sample_uid", "", "KeywordIndex"),
194
)
195
196
COLUMNS = (
197
    # catalog, column name
198
    ("bika_catalog", "path"),
199
    ("bika_catalog", "UID"),
200
    ("bika_catalog", "id"),
201
    ("bika_catalog", "getId"),
202
    ("bika_catalog", "Type"),
203
    ("bika_catalog", "portal_type"),
204
    ("bika_catalog", "creator"),
205
    ("bika_catalog", "Created"),
206
    ("bika_catalog", "Title"),
207
    ("bika_catalog", "Description"),
208
    ("bika_catalog", "sortable_title"),
209
    ("bika_catalog", "getClientTitle"),
210
    ("bika_catalog", "getClientID"),
211
    ("bika_catalog", "getClientBatchID"),
212
    ("bika_catalog", "getDateReceived"),
213
    ("bika_catalog", "getDateSampled"),
214
    ("bika_catalog", "review_state"),
215
    ("bika_catalog", "getProgress"),
216
217
    ("bika_setup_catalog", "path"),
218
    ("bika_setup_catalog", "UID"),
219
    ("bika_setup_catalog", "id"),
220
    ("bika_setup_catalog", "getId"),
221
    ("bika_setup_catalog", "Type"),
222
    ("bika_setup_catalog", "portal_type"),
223
    ("bika_setup_catalog", "Title"),
224
    ("bika_setup_catalog", "Description"),
225
    ("bika_setup_catalog", "title"),
226
    ("bika_setup_catalog", "sortable_title"),
227
    ("bika_setup_catalog", "description"),
228
    ("bika_setup_catalog", "review_state"),
229
    ("bika_setup_catalog", "getCategoryTitle"),
230
    ("bika_setup_catalog", "getCategoryUID"),
231
    ("bika_setup_catalog", "getClientUID"),
232
    ("bika_setup_catalog", "getKeyword"),
233
234
    ("portal_catalog", "Analyst"),
235
)
236
237
ALLOWED_STYLES = [
238
    "color",
239
    "background-color"
240
]
241
242
243
def pre_install(portal_setup):
244
    """Runs before the first import step of the *default* profile
245
246
    This handler is registered as a *pre_handler* in the generic setup profile
247
248
    :param portal_setup: SetupTool
249
    """
250
    logger.info("SENAITE PRE-INSTALL handler [BEGIN]")
251
252
    # https://docs.plone.org/develop/addons/components/genericsetup.html#custom-installer-code-setuphandlers-py
253
    profile_id = PROFILE_ID
254
255
    context = portal_setup._getImportContext(profile_id)
256
    portal = context.getSite()  # noqa
257
258
    logger.info("SENAITE PRE-INSTALL handler [DONE]")
259
260
261
def post_install(portal_setup):
262
    """Runs after the last import step of the *default* profile
263
264
    This handler is registered as a *post_handler* in the generic setup profile
265
266
    :param portal_setup: SetupTool
267
    """
268
    logger.info("SENAITE POST-INSTALL handler [BEGIN]")
269
270
    # https://docs.plone.org/develop/addons/components/genericsetup.html#custom-installer-code-setuphandlers-py
271
    profile_id = PROFILE_ID
272
273
    context = portal_setup._getImportContext(profile_id)
274
    portal = context.getSite()  # noqa
275
276
    logger.info("SENAITE POST-INSTALL handler [DONE]")
277
278
279
def setup_handler(context):
280
    """SENAITE setup handler
281
    """
282
283
    if context.readDataFile("bika.lims_various.txt") is None:
284
        return
285
286
    logger.info("SENAITE setup handler [BEGIN]")
287
288
    portal = context.getSite()
289
290
    # Run Installers
291
    hide_navbar_items(portal)
292
    reindex_content_structure(portal)
293
    setup_groups(portal)
294
    setup_catalog_mappings(portal)
295
    setup_core_catalogs(portal)
296
    add_dexterity_portal_items(portal)
297
    add_dexterity_setup_items(portal)
298
    # XXX P5: Fix HTML filtering
299
    # setup_html_filter(portal)
300
301
    # Run after all catalogs have been setup
302
    setup_auditlog_catalog(portal)
303
304
    # Set CMF Form actions
305
    setup_form_controller_actions(portal)
306
307
    logger.info("SENAITE setup handler [DONE]")
308
309
310
def hide_navbar_items(portal):
311
    """Hide root items in navigation
312
    """
313
    logger.info("*** Hide Navigation Items ***")
314
315
    # Get the list of object ids for portal
316
    object_ids = portal.objectIds()
317
    object_ids = filter(lambda id: id in object_ids, NAV_BAR_ITEMS_TO_HIDE)
318
    for object_id in object_ids:
319
        item = portal[object_id]
320
        item.setExcludeFromNav(True)
321
        item.reindexObject()
322
323
324
def reindex_content_structure(portal):
325
    """Reindex contents generated by Generic Setup
326
    """
327
    logger.info("*** Reindex content structure ***")
328
329
    def reindex(obj, recurse=False):
330
        # skip catalog tools etc.
331
        if api.is_object(obj):
332
            obj.reindexObject()
333
        if recurse and hasattr(aq_base(obj), "objectValues"):
334
            map(reindex, obj.objectValues())
335
336
    setup = api.get_setup()
337
    setupitems = setup.objectValues()
338
    rootitems = portal.objectValues()
339
340
    for obj in itertools.chain(setupitems, rootitems):
341
        logger.info("Reindexing {}".format(repr(obj)))
342
        reindex(obj)
343
344
345
def setup_groups(portal):
346
    """Setup roles and groups
347
    """
348
    logger.info("*** Setup Roles and Groups ***")
349
350
    portal_groups = api.get_tool("portal_groups")
351
352
    for gdata in GROUPS:
353
        group_id = gdata["id"]
354
        # create the group and grant the roles
355
        if group_id not in portal_groups.listGroupIds():
356
            logger.info("+++ Adding group {title} ({id})".format(**gdata))
357
            portal_groups.addGroup(group_id,
358
                                   title=gdata["title"],
359
                                   roles=gdata["roles"])
360
        # grant the roles to the existing group
361
        else:
362
            ploneapi.group.grant_roles(
363
                groupname=gdata["id"],
364
                roles=gdata["roles"],)
365
            logger.info("+++ Granted group {title} ({id}) the roles {roles}"
366
                        .format(**gdata))
367
368
369
def setup_catalog_mappings(portal):
370
    """Setup portal_type -> catalog mappings
371
    """
372
    logger.info("*** Setup Catalog Mappings ***")
373
374
    at = api.get_tool("archetype_tool")
375
    for portal_type, catalogs in CATALOG_MAPPINGS:
376
        at.setCatalogsByType(portal_type, catalogs)
377
378
379
def setup_core_catalogs(portal):
380
    """Setup core catalogs
381
    """
382
    logger.info("*** Setup Core Catalogs ***")
383
384
    # Setting up all LIMS catalogs defined in catalog folder
385
    setup_catalogs(portal, getCatalogDefinitions())
386
387
    to_reindex = []
388
    for catalog, name, attribute, meta_type in INDEXES:
389
        c = api.get_tool(catalog)
390
        indexes = c.indexes()
391
        if name in indexes:
392
            logger.info("*** Index '%s' already in Catalog [SKIP]" % name)
393
            continue
394
395
        logger.info("*** Adding Index '%s' for field '%s' to catalog ..."
396
                    % (meta_type, name))
397
398
        # do we still need ZCTextIndexes?
399
        if meta_type == "ZCTextIndex":
400
            addZCTextIndex(c, name)
401
        else:
402
            c.addIndex(name, meta_type)
403
404
        # get the new created index
405
        index = c._catalog.getIndex(name)
406
        # set the indexed attributes
407
        if hasattr(index, "indexed_attrs"):
408
            index.indexed_attrs = [attribute or name]
409
410
        to_reindex.append((c, name))
411
        logger.info("*** Added Index '%s' for field '%s' to catalog [DONE]"
412
                    % (meta_type, name))
413
414
    # catalog columns
415
    for catalog, name in COLUMNS:
416
        c = api.get_tool(catalog)
417
        if name not in c.schema():
418
            logger.info("*** Adding Column '%s' to catalog '%s' ..."
419
                        % (name, catalog))
420
            c.addColumn(name)
421
            logger.info("*** Added Column '%s' to catalog '%s' [DONE]"
422
                        % (name, catalog))
423
        else:
424
            logger.info("*** Column '%s' already in catalog '%s'  [SKIP]"
425
                        % (name, catalog))
426
            continue
427
428
    for catalog, name in to_reindex:
429
        logger.info("*** Indexing new index '%s' ..." % name)
430
        catalog.manage_reindexIndex(name)
431
        logger.info("*** Indexing new index '%s' [DONE]" % name)
432
433
434
def setup_auditlog_catalog(portal):
435
    """Setup auditlog catalog
436
    """
437
    logger.info("*** Setup Audit Log Catalog ***")
438
439
    catalog_id = auditlog_catalog.CATALOG_AUDITLOG
440
    catalog = api.get_tool(catalog_id)
441
442
    for name, meta_type in auditlog_catalog._indexes.iteritems():
443
        indexes = catalog.indexes()
444
        if name in indexes:
445
            logger.info("*** Index '%s' already in Catalog [SKIP]" % name)
446
            continue
447
448
        logger.info("*** Adding Index '%s' for field '%s' to catalog ..."
449
                    % (meta_type, name))
450
451
        catalog.addIndex(name, meta_type)
452
453
        # Setup TextIndexNG3 for listings
454
        # XXX is there another way to do this?
455
        if meta_type == "TextIndexNG3":
456
            index = catalog._catalog.getIndex(name)
457
            index.index.default_encoding = "utf-8"
458
            index.index.query_parser = "txng.parsers.en"
459
            index.index.autoexpand = "always"
460
            index.index.autoexpand_limit = 3
461
462
        logger.info("*** Added Index '%s' for field '%s' to catalog [DONE]"
463
                    % (meta_type, name))
464
465
    # Attach the catalog to all known portal types
466
    at = api.get_tool("archetype_tool")
467
    pt = api.get_tool("portal_types")
468
469
    for portal_type in pt.listContentTypes():
470
        catalogs = at.getCatalogsByType(portal_type)
471
        if catalog not in catalogs:
472
            new_catalogs = map(lambda c: c.getId(), catalogs) + [catalog_id]
473
            at.setCatalogsByType(portal_type, new_catalogs)
474
            logger.info("*** Adding catalog '{}' for '{}'".format(
475
                catalog_id, portal_type))
476
477
478
def setup_form_controller_actions(portal):
479
    """Setup custom CMF Form actions
480
    """
481
    logger.info("*** Setup Form Controller custom actions ***")
482
    fc_tool = api.get_tool("portal_form_controller")
483
484
    # Redirect the user to Worksheets listing view after the "remove" action
485
    # from inside Worksheet context is pressed
486
    # https://github.com/senaite/senaite.core/pull/1480
487
    fc_tool.addFormAction(
488
        object_id="content_status_modify",
489
        status="success",
490
        context_type="Worksheet",
491
        button=None,
492
        action_type="redirect_to",
493
        action_arg="python:object.aq_inner.aq_parent.absolute_url()")
494
495
496
def add_dexterity_portal_items(portal):
497
    """Adds the Dexterity Container in the Site folder
498
499
    N.B.: We do this in code, because adding this as Generic Setup Profile in
500
          `profiles/default/structure` flushes the contents on every import.
501
    """
502
    # Tuples of ID, Title, FTI
503
    items = [
504
        ("samples",  # ID
505
         "Samples",  # Title
506
         "Samples"),  # FTI
507
    ]
508
    add_dexterity_items(portal, items)
509
510
    # Move Samples after Clients nav item
511
    position = portal.getObjectPosition("clients")
512
    portal.moveObjectToPosition("samples", position + 1)
513
    portal.plone_utils.reindexOnReorder(portal)
514
515
516
def add_dexterity_setup_items(portal):
517
    """Adds the Dexterity Container in the Setup Folder
518
519
    N.B.: We do this in code, because adding this as Generic Setup Profile in
520
          `profiles/default/structure` flushes the contents on every import.
521
    """
522
    # Tuples of ID, Title, FTI
523
    items = [
524
        ("dynamic_analysisspecs",  # ID
525
         "Dynamic Analysis Specifications",  # Title
526
         "DynamicAnalysisSpecs"),  # FTI
527
528
        ("interpretation_templates",
529
         "Interpretation Templates",
530
         "InterpretationTemplates")
531
    ]
532
    setup = api.get_setup()
533
    add_dexterity_items(setup, items)
534
535
536
def add_dexterity_items(container, items):
537
    """Adds a dexterity item, usually a folder in the container
538
    :param container: container of the items to add
539
    :param items: tuple of Id, Title, FTI
540
    """
541
    pt = api.get_tool("portal_types")
542
    ti = pt.getTypeInfo(container)
543
544
    # Temporary allow content type creation in content
545
    ftis = map(lambda item: item[2], items)
546
    temporary_allow_type(container, ftis)
547
548
    for id, title, fti in items:
549
        obj = container.get(id)
550
        if obj is None:
551
            with temporary_allow_type(container, fti) as ct:
552
                obj = api.create(ct, fti, id=id, title=title)
553
            obj.reindexObject()
554
        else:
555
            obj.setTitle(title)
556
            obj.reindexObject()
557
558
559
def setup_html_filter(portal):
560
    """Setup HTML filtering for resultsinterpretations
561
    """
562
    logger.info("*** Setup HTML Filter ***")
563
    # XXX P5: Fix ImportError: No module named controlpanel.filter
564
    from plone.app.controlpanel.filter import IFilterSchema
565
    # bypass the broken API from portal_transforms
566
    adapter = IFilterSchema(portal)
567
    style_whitelist = adapter.style_whitelist
568
    for style in ALLOWED_STYLES:
569
        logger.info("Allow style '{}'".format(style))
570
        if style not in style_whitelist:
571
            style_whitelist.append(style)
572
    adapter.style_whitelist = style_whitelist
573