Passed
Pull Request — 2.x (#1872)
by Jordi
06:13
created

senaite.core.setuphandlers   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 470
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 47
eloc 299
dl 0
loc 470
rs 8.64
c 0
b 0
f 0

15 Functions

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