Passed
Pull Request — 2.x (#1911)
by Jordi
12:55 queued 07:53
created

senaite.core.setuphandlers   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 528
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 53
eloc 328
dl 0
loc 528
rs 6.96
c 0
b 0
f 0

18 Functions

Rating   Name   Duplication   Size   Complexity  
A install() 0 45 4
A setup_form_controller_more_action() 0 15 1
A post_install() 0 18 1
A add_catalog_index() 0 12 2
B setup_other_catalogs() 0 28 7
A add_catalog_column() 0 10 2
A setup_content_structure() 0 6 1
A setup_auditlog_catalog_mappings() 0 23 5
A add_dexterity_items() 0 13 4
A add_dexterity_portal_items() 0 18 1
A setup_catalog_mappings() 0 12 3
A remove_default_content() 0 10 3
A setup_markup_schema() 0 10 2
A reindex_catalog_index() 0 7 1
A add_dexterity_setup_items() 0 22 1
D setup_core_catalogs() 0 57 12
A _run_import_step() 0 7 1
A pre_install() 0 15 1

1 Method

Rating   Name   Duplication   Size   Complexity  
A HiddenProfiles.getNonInstallableProfiles() 0 22 1

How to fix   Complexity   

Complexity

Complex classes like senaite.core.setuphandlers often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
from Acquisition import aq_base
22
from bika.lims import api
23
from bika.lims.setuphandlers import reindex_content_structure
24
from bika.lims.setuphandlers import setup_form_controller_actions
25
from bika.lims.setuphandlers import setup_groups
26
from plone.dexterity.fti import DexterityFTI
27
from plone.registry.interfaces import IRegistry
28
from Products.CMFPlone.utils import get_installer
29
from Products.GenericSetup.utils import _resolveDottedName
30
from senaite.core import logger
31
from senaite.core.api.catalog import add_column
32
from senaite.core.api.catalog import add_index
33
from senaite.core.api.catalog import get_columns
34
from senaite.core.api.catalog import get_indexes
35
from senaite.core.api.catalog import reindex_index
36
from senaite.core.catalog import AUDITLOG_CATALOG
37
from senaite.core.catalog import AnalysisCatalog
38
from senaite.core.catalog import AuditlogCatalog
39
from senaite.core.catalog import AutoImportLogCatalog
40
from senaite.core.catalog import ReportCatalog
41
from senaite.core.catalog import SampleCatalog
42
from senaite.core.catalog import SenaiteCatalog
43
from senaite.core.catalog import SetupCatalog
44
from senaite.core.catalog import WorksheetCatalog
45
from senaite.core.config import PROFILE_ID
46
from senaite.core.upgrade.utils import temporary_allow_type
47
from zope.component import getUtility
48
from zope.interface import implementer
49
50
try:
51
    from Products.CMFPlone.interfaces import IMarkupSchema
52
    from Products.CMFPlone.interfaces import INonInstallable
53
except ImportError:
54
    from zope.interface import Interface
55
    IMarkupSchema = None
56
57
    class INonInstallable(Interface):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable Interface does not seem to be defined.
Loading history...
58
        pass
59
60
61
@implementer(INonInstallable)
62
class HiddenProfiles(object):
63
    def getNonInstallableProfiles(self):
64
        """Hide all profiles from site-creation and quickinstaller (not ZMI)"""
65
        return [
66
            # Leave visible to allow upgrade via the Plone Add-on controlpanel
67
            # "bika.lims:default",
68
69
            # hide install profiles that come with Plone
70
            "Products.CMFPlacefulWorkflow:CMFPlacefulWorkflow",
71
            "Products.CMFPlacefulWorkflow:base",
72
            "Products.CMFPlacefulWorkflow:uninstall",
73
            "Products.DataGridField:default",
74
            "Products.DataGridField:example",
75
            "Products.TextIndexNG3:default",
76
            "archetypes.multilingual:default",
77
            "archetypes.referencebrowserwidget:default",
78
            "collective.js.jqueryui:default"
79
            "plone.app.iterate:default",
80
            "plone.app.iterate:plone.app.iterate",
81
            "plone.app.iterate:test",
82
            "plone.app.iterate:uninstall",
83
            "plone.app.jquery:default",
84
            "plonetheme.barceloneta:default",
85
        ]
86
87
88
CONTENTS_TO_DELETE = (
89
    # List of items to delete
90
    "Members",
91
    "news",
92
    "events",
93
)
94
95
CATALOGS = (
96
    AnalysisCatalog,
97
    AuditlogCatalog,
98
    AutoImportLogCatalog,
99
    SampleCatalog,
100
    SenaiteCatalog,
101
    SetupCatalog,
102
    WorksheetCatalog,
103
    ReportCatalog,
104
)
105
106
INDEXES = (
107
    # catalog, id, indexed attribute, type
108
    ("portal_catalog", "Analyst", "", "FieldIndex"),
109
    ("portal_catalog", "analysisRequestTemplates", "", "FieldIndex"),
110
    ("portal_catalog", "getFullname", "", "FieldIndex"),
111
    ("portal_catalog", "getName", "", "FieldIndex"),
112
    ("portal_catalog", "getParentUID", "", "FieldIndex"),
113
    ("portal_catalog", "getUsername", "", "FieldIndex"),
114
    ("portal_catalog", "is_active", "", "BooleanIndex"),
115
    ("portal_catalog", "path", "getPhysicalPath", "ExtendedPathIndex"),
116
    ("portal_catalog", "review_state", "", "FieldIndex"),
117
    ("portal_catalog", "sample_uid", "", "KeywordIndex"),
118
    ("portal_catalog", "title", "", "FieldIndex"),
119
)
120
121
COLUMNS = (
122
    # catalog, column name
123
    ("portal_catalog", "analysisRequestTemplates"),
124
    ("portal_catalog", "review_state"),
125
    ("portal_catalog", "getClientID"),
126
    ("portal_catalog", "Analyst"),
127
)
128
129
CATALOG_MAPPINGS = (
130
    # portal_type, catalog_ids
131
    ("ARTemplate", ["senaite_catalog_setup", "portal_catalog"]),
132
    ("AnalysisCategory", ["senaite_catalog_setup", "portal_catalog"]),
133
    ("AnalysisProfile", ["senaite_catalog_setup", "portal_catalog"]),
134
    ("AnalysisService", ["senaite_catalog_setup", "portal_catalog"]),
135
    ("AnalysisSpec", ["senaite_catalog_setup", "portal_catalog"]),
136
    ("Attachment", ["senaite_catalog", "portal_catalog"]),
137
    ("AttachmentType", ["senaite_catalog_setup", "portal_catalog"]),
138
    ("Batch", ["senaite_catalog", "portal_catalog"]),
139
    ("BatchLabel", ["senaite_catalog_setup", "portal_catalog"]),
140
    ("Calculation", ["senaite_catalog_setup", "portal_catalog"]),
141
    ("Container", ["senaite_catalog_setup", "portal_catalog"]),
142
    ("ContainerType", ["senaite_catalog_setup", "portal_catalog"]),
143
    ("Department", ["senaite_catalog_setup", "portal_catalog"]),
144
    ("Instrument", ["senaite_catalog_setup", "portal_catalog"]),
145
    ("InstrumentType", ["senaite_catalog_setup", "portal_catalog"]),
146
    ("LabContact", ["senaite_catalog_setup", "portal_catalog"]),
147
    ("LabProduct", ["senaite_catalog_setup", "portal_catalog"]),
148
    ("Manufacturer", ["senaite_catalog_setup", "portal_catalog"]),
149
    ("Method", ["senaite_catalog_setup", "portal_catalog"]),
150
    ("Multifile", ["senaite_catalog_setup", "portal_catalog"]),
151
    ("Preservation", ["senaite_catalog_setup", "portal_catalog"]),
152
    ("ReferenceDefinition", ["senaite_catalog_setup", "portal_catalog"]),
153
    ("ReferenceSample", ["senaite_catalog", "portal_catalog"]),
154
    ("SampleCondition", ["senaite_catalog_setup", "portal_catalog"]),
155
    ("SampleMatrix", ["senaite_catalog_setup", "portal_catalog"]),
156
    ("SamplePoint", ["senaite_catalog_setup", "portal_catalog"]),
157
    ("SampleType", ["senaite_catalog_setup", "portal_catalog"]),
158
    ("SamplingDeviation", ["senaite_catalog_setup", "portal_catalog"]),
159
    ("StorageLocation", ["senaite_catalog_setup", "portal_catalog"]),
160
    ("SubGroup", ["senaite_catalog_setup", "portal_catalog"]),
161
    ("Supplier", ["senaite_catalog_setup", "portal_catalog"]),
162
    ("WorksheetTemplate", ["senaite_catalog_setup", "portal_catalog"]),
163
)
164
165
166
def install(context):
167
    """Install handler
168
    """
169
    if context.readDataFile("senaite.core.txt") is None:
170
        return
171
172
    logger.info("SENAITE CORE install handler [BEGIN]")
173
    portal = context.getSite()
174
175
    # Run required import steps
176
    _run_import_step(portal, "skins")
177
    _run_import_step(portal, "browserlayer")
178
    _run_import_step(portal, "rolemap")
179
    _run_import_step(portal, "typeinfo")
180
    _run_import_step(portal, "factorytool")
181
    _run_import_step(portal, "workflow", "profile-senaite.core:default")
182
    _run_import_step(portal, "typeinfo", "profile-senaite.core:default")
183
184
    # skip installers if already installed
185
    qi = get_installer(portal)
186
    profiles = ["bika.lims", "senaite.core"]
187
    if any(map(lambda p: qi.is_product_installed(p), profiles)):
0 ignored issues
show
introduced by
The variable qi does not seem to be defined for all execution paths.
Loading history...
188
        logger.info("SENAITE CORE already installed [SKIP]")
189
        return
190
191
    # Run Installers
192
    setup_groups(portal)
193
    remove_default_content(portal)
194
    # setup catalogs
195
    setup_core_catalogs(portal)
196
    setup_other_catalogs(portal)
197
    setup_catalog_mappings(portal)
198
    setup_auditlog_catalog_mappings(portal)
199
    setup_content_structure(portal)
200
    add_dexterity_portal_items(portal)
201
    add_dexterity_setup_items(portal)
202
203
    # Set CMF Form actions
204
    setup_form_controller_actions(portal)
205
    setup_form_controller_more_action(portal)
206
207
    # Setup markup default and allowed schemas
208
    setup_markup_schema(portal)
209
210
    logger.info("SENAITE CORE install handler [DONE]")
211
212
213
def add_dexterity_setup_items(portal):
214
    """Adds the Dexterity Container in the Setup Folder
215
216
    N.B.: We do this in code, because adding this as Generic Setup Profile in
217
          `profiles/default/structure` flushes the contents on every import.
218
    """
219
    # Tuples of ID, Title, FTI
220
    items = [
221
        ("dynamic_analysisspecs",  # ID
222
         "Dynamic Analysis Specifications",  # Title
223
         "DynamicAnalysisSpecs"),  # FTI
224
225
        ("interpretation_templates",
226
         "Interpretation Templates",
227
         "InterpretationTemplates"),
228
229
        ("sample_containers",
230
         "Sample Containers",
231
         "SampleContainers")
232
    ]
233
    setup = api.get_setup()
234
    add_dexterity_items(setup, items)
235
236
237
def add_dexterity_portal_items(portal):
238
    """Adds the Dexterity Container in the Site folder
239
240
    N.B.: We do this in code, because adding this as Generic Setup Profile in
241
          `profiles/default/structure` flushes the contents on every import.
242
    """
243
    # Tuples of ID, Title, FTI
244
    items = [
245
        ("samples",  # ID
246
         "Samples",  # Title
247
         "Samples"),  # FTI
248
    ]
249
    add_dexterity_items(portal, items)
250
251
    # Move Samples after Clients nav item
252
    position = portal.getObjectPosition("clients")
253
    portal.moveObjectToPosition("samples", position + 1)
254
    portal.plone_utils.reindexOnReorder(portal)
255
256
257
def add_dexterity_items(container, items):
258
    """Adds a dexterity item, usually a folder in the container
259
    :param container: container of the items to add
260
    :param items: tuple of Id, Title, FTI
261
    """
262
    for id, title, fti in items:
263
        obj = container.get(id)
264
        if obj is None:
265
            with temporary_allow_type(container, fti) as ct:
266
                obj = api.create(ct, fti, id=id, title=title)
267
        else:
268
            obj.setTitle(title)
269
        obj.reindexObject()
270
271
272
def setup_core_catalogs(portal, catalog_classes=None, reindex=True):
273
    """Setup core catalogs
274
    """
275
    logger.info("*** Setup core catalogs ***")
276
    at = api.get_tool("archetype_tool")
277
278
    # allow add-ons to use this handler with own catalogs
279
    if catalog_classes is None:
280
        catalog_classes = CATALOGS
281
282
    # contains tuples of (catalog, index) pairs
283
    to_reindex = []
284
285
    for cls in catalog_classes:
286
        module = _resolveDottedName(cls.__module__)
287
288
        # get the required attributes from the module
289
        catalog_id = module.CATALOG_ID
290
        catalog_indexes = module.INDEXES
291
        catalog_columns = module.COLUMNS
292
        catalog_types = module.TYPES
293
294
        catalog = getattr(aq_base(portal), catalog_id, None)
295
        if catalog is None:
296
            catalog = cls()
297
            catalog._setId(catalog_id)
298
            portal._setObject(catalog_id, catalog)
299
300
        # catalog indexes
301
        for idx_id, idx_attr, idx_type in catalog_indexes:
302
            if add_catalog_index(catalog, idx_id, idx_attr, idx_type):
303
                to_reindex.append((catalog, idx_id))
304
            else:
305
                continue
306
307
        # catalog columns
308
        for column in catalog_columns:
309
            add_catalog_column(catalog, column)
310
311
        if not reindex:
312
            logger.info("*** Skipping reindex of new indexes")
313
            return
314
315
        # map allowed types to this catalog in archetype_tool
316
        for portal_type in catalog_types:
317
            # check existing catalogs
318
            catalogs = at.getCatalogsByType(portal_type)
319
            if catalog not in catalogs:
320
                existing = list(map(lambda c: c.getId(), catalogs))
321
                new_catalogs = existing + [catalog_id]
322
                at.setCatalogsByType(portal_type, new_catalogs)
323
                logger.info("*** Mapped catalog '%s' for type '%s'"
324
                            % (catalog_id, portal_type))
325
326
    # reindex new indexes
327
    for catalog, idx_id in to_reindex:
328
        reindex_catalog_index(catalog, idx_id)
329
330
331
def setup_other_catalogs(portal, indexes=None, columns=None):
332
    logger.info("*** Setup other catalogs ***")
333
334
    # contains tuples of (catalog, index) pairs
335
    to_reindex = []
336
337
    # allow add-ons to use this handler with other index/column definitions
338
    if indexes is None:
339
        indexes = INDEXES
340
    if columns is None:
341
        columns = COLUMNS
342
343
    # catalog indexes
344
    for catalog, idx_id, idx_attr, idx_type in indexes:
345
        catalog = api.get_tool(catalog)
346
        if add_catalog_index(catalog, idx_id, idx_attr, idx_type):
347
            to_reindex.append((catalog, idx_id))
348
        else:
349
            continue
350
351
    # catalog columns
352
    for catalog, column in columns:
353
        catalog = api.get_tool(catalog)
354
        add_catalog_column(catalog, column)
355
356
    # reindex new indexes
357
    for catalog, idx_id in to_reindex:
358
        reindex_catalog_index(catalog, idx_id)
359
360
361
def reindex_catalog_index(catalog, index):
362
    catalog_id = catalog.id
363
    logger.info("*** Indexing new index '%s' in '%s' ..."
364
                % (index, catalog_id))
365
    reindex_index(catalog, index)
366
    logger.info("*** Indexing new index '%s' in '%s' [DONE]"
367
                % (index, catalog_id))
368
369
370
def add_catalog_index(catalog, idx_id, idx_attr, idx_type):
371
    indexes = get_indexes(catalog)
372
    # check if the index exists
373
    if idx_id in indexes:
374
        logger.info("*** %s '%s' already in catalog '%s'"
375
                    % (idx_type, idx_id, catalog.id))
376
        return False
377
    # create the index
378
    add_index(catalog, idx_id, idx_type, indexed_attrs=idx_attr)
379
    logger.info("*** Added %s '%s' for catalog '%s'"
380
                % (idx_type, idx_id, catalog.id))
381
    return True
382
383
384
def add_catalog_column(catalog, column):
385
    columns = get_columns(catalog)
386
    if column in columns:
387
        logger.info("*** Column '%s' already in catalog '%s'"
388
                    % (column, catalog.id))
389
        return False
390
    add_column(catalog, column)
391
    logger.info("*** Added column '%s' to catalog '%s'"
392
                % (column, catalog.id))
393
    return True
394
395
396
def setup_catalog_mappings(portal, catalog_mappings=None):
397
    """Setup portal_type -> catalog mappings
398
    """
399
    logger.info("*** Setup Catalog Mappings ***")
400
401
    # allow add-ons to use this handler with own mappings
402
    if catalog_mappings is None:
403
        catalog_mappings = CATALOG_MAPPINGS
404
405
    at = api.get_tool("archetype_tool")
406
    for portal_type, catalogs in catalog_mappings:
407
        at.setCatalogsByType(portal_type, catalogs)
408
409
410
def setup_auditlog_catalog_mappings(portal):
411
    """Map auditlog catalog to all AT content types
412
    """
413
    at = api.get_tool("archetype_tool")
414
    pt = api.get_tool("portal_types")
415
    portal_types = pt.listContentTypes()
416
417
    # map all known AT types to the auditlog catalog
418
    auditlog_catalog = api.get_tool(AUDITLOG_CATALOG)
419
    for portal_type in portal_types:
420
421
        # Do not map DX types into archetypes tool
422
        fti = pt.getTypeInfo(portal_type)
423
        if isinstance(fti, DexterityFTI):
424
            continue
425
426
        catalogs = at.getCatalogsByType(portal_type)
427
        if auditlog_catalog not in catalogs:
428
            existing_catalogs = list(map(lambda c: c.getId(), catalogs))
429
            new_catalogs = existing_catalogs + [AUDITLOG_CATALOG]
430
            at.setCatalogsByType(portal_type, new_catalogs)
431
            logger.info("*** Adding catalog '{}' for '{}'".format(
432
                AUDITLOG_CATALOG, portal_type))
433
434
435
def remove_default_content(portal):
436
    """Remove default Plone contents
437
    """
438
    logger.info("*** Remove Default Content ***")
439
440
    # Get the list of object ids for portal
441
    object_ids = portal.objectIds()
442
    delete_ids = filter(lambda id: id in object_ids, CONTENTS_TO_DELETE)
443
    if delete_ids:
444
        portal.manage_delObjects(ids=list(delete_ids))
445
446
447
def setup_content_structure(portal):
448
    """Install the base content structure
449
    """
450
    logger.info("*** Install SENAITE Content Types ***")
451
    _run_import_step(portal, "content")
452
    reindex_content_structure(portal)
453
454
455
def setup_form_controller_more_action(portal):
456
    """Install form controller actions for ported record widgets
457
458
    Code taken from Products.ATExtensions
459
    """
460
    logger.info("*** Install SENAITE Form Controller Actions ***")
461
    pfc = portal.portal_form_controller
462
    pfc.addFormValidators(
463
        "base_edit", "", "more", "")
464
    pfc.addFormAction(
465
        "base_edit", "success", "", "more", "traverse_to", "string:more_edit")
466
    pfc.addFormValidators(
467
        "atct_edit", "", "more", "",)
468
    pfc.addFormAction(
469
        "atct_edit", "success", "", "more", "traverse_to", "string:more_edit")
470
471
472
def _run_import_step(portal, name, profile="profile-bika.lims:default"):
473
    """Helper to install a GS import step from the given profile
474
    """
475
    logger.info("*** Running import step '{}' from profile '{}' ***"
476
                .format(name, profile))
477
    setup = portal.portal_setup
478
    setup.runImportStepFromProfile(profile, name)
479
480
481
def pre_install(portal_setup):
482
    """Runs berfore the first import step of the *default* profile
483
484
    This handler is registered as a *pre_handler* in the generic setup profile
485
486
    :param portal_setup: SetupTool
487
    """
488
    logger.info("SENAITE CORE pre-install handler [BEGIN]")
489
490
    # https://docs.plone.org/develop/addons/components/genericsetup.html#custom-installer-code-setuphandlers-py
491
    profile_id = PROFILE_ID
492
    context = portal_setup._getImportContext(profile_id)
493
    portal = context.getSite()  # noqa
494
495
    logger.info("SENAITE CORE pre-install handler [DONE]")
496
497
498
def post_install(portal_setup):
499
    """Runs after the last import step of the *default* profile
500
501
    This handler is registered as a *post_handler* in the generic setup profile
502
503
    :param portal_setup: SetupTool
504
    """
505
    logger.info("SENAITE CORE post install handler [BEGIN]")
506
507
    # https://docs.plone.org/develop/addons/components/genericsetup.html#custom-installer-code-setuphandlers-py
508
    profile_id = PROFILE_ID
509
    context = portal_setup._getImportContext(profile_id)
510
    portal = context.getSite()  # noqa
511
512
    # always apply the skins profile last to ensure our layers are first
513
    _run_import_step(portal, "skins", profile=profile_id)
514
515
    logger.info("SENAITE CORE post install handler [DONE]")
516
517
518
def setup_markup_schema(portal):
519
    """Sets the default and allowed markup schemas for RichText widgets
520
    """
521
    if not IMarkupSchema:
522
        return
523
524
    registry = getUtility(IRegistry, context=portal)
525
    settings = registry.forInterface(IMarkupSchema, prefix='plone')
526
    settings.default_type = u"text/html"
527
    settings.allowed_types = ("text/html", )
528