Passed
Pull Request — 2.x (#1872)
by Jordi
04:50
created

senaite.core.setuphandlers   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 463
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 46
eloc 295
dl 0
loc 463
rs 8.72
c 0
b 0
f 0

15 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 setup_content_structure() 0 6 1
A setup_auditlog_catalog_mappings() 0 17 4
A remove_default_content() 0 10 3
A setup_markup_schema() 0 10 2
A _run_import_step() 0 7 1
A pre_install() 0 15 1
A add_catalog_index() 0 12 2
B setup_other_catalogs() 0 28 7
A add_catalog_column() 0 10 2
A setup_catalog_mappings() 0 12 3
A reindex_catalog_index() 0 7 1
D setup_core_catalogs() 0 57 12

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.registry.interfaces import IRegistry
29
from Products.CMFPlone.utils import get_installer
30
from Products.GenericSetup.utils import _resolveDottedName
31
from senaite.core import logger
32
from senaite.core.api.catalog import add_column
33
from senaite.core.api.catalog import add_index
34
from senaite.core.api.catalog import get_columns
35
from senaite.core.api.catalog import get_indexes
36
from senaite.core.api.catalog import reindex_index
37
from senaite.core.catalog import AUDITLOG_CATALOG
38
from senaite.core.catalog import AnalysisCatalog
39
from senaite.core.catalog import AuditlogCatalog
40
from senaite.core.catalog import AutoImportLogCatalog
41
from senaite.core.catalog import ReportCatalog
42
from senaite.core.catalog import SampleCatalog
43
from senaite.core.catalog import SenaiteCatalog
44
from senaite.core.catalog import SetupCatalog
45
from senaite.core.catalog import WorksheetCatalog
46
from senaite.core.config import PROFILE_ID
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 setup_core_catalogs(portal, catalog_classes=None, reindex=True):
214
    """Setup core catalogs
215
    """
216
    logger.info("*** Setup core catalogs ***")
217
    at = api.get_tool("archetype_tool")
218
219
    # allow add-ons to use this handler with own catalogs
220
    if catalog_classes is None:
221
        catalog_classes = CATALOGS
222
223
    for cls in catalog_classes:
224
        module = _resolveDottedName(cls.__module__)
225
226
        # get the required attributes from the module
227
        catalog_id = module.CATALOG_ID
228
        catalog_indexes = module.INDEXES
229
        catalog_columns = module.COLUMNS
230
        catalog_types = module.TYPES
231
232
        catalog = getattr(aq_base(portal), catalog_id, None)
233
        if catalog is None:
234
            catalog = cls()
235
            catalog._setId(catalog_id)
236
            portal._setObject(catalog_id, catalog)
237
238
        # contains tuples of (catalog, index) pairs
239
        to_reindex = []
240
241
        # catalog indexes
242
        for idx_id, idx_attr, idx_type in catalog_indexes:
243
            if add_catalog_index(catalog, idx_id, idx_attr, idx_type):
244
                to_reindex.append((catalog, idx_id))
245
            else:
246
                continue
247
248
        # catalog columns
249
        for column in catalog_columns:
250
            add_catalog_column(catalog, column)
251
252
        if not reindex:
253
            logger.info("*** Skipping reindex of new indexes")
254
            return
255
256
        # map allowed types to this catalog in archetype_tool
257
        for portal_type in catalog_types:
258
            # check existing catalogs
259
            catalogs = at.getCatalogsByType(portal_type)
260
            if catalog not in catalogs:
261
                existing = list(map(lambda c: c.getId(), catalogs))
262
                new_catalogs = existing + [catalog_id]
263
                at.setCatalogsByType(portal_type, new_catalogs)
264
                logger.info("*** Mapped catalog '%s' for type '%s'"
265
                            % (catalog_id, portal_type))
266
267
    # reindex new indexes
268
    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 223 is not entered. Are you sure this can never be the case?
Loading history...
269
        reindex_catalog_index(catalog, idx_id)
270
271
272
def setup_other_catalogs(portal, indexes=None, columns=None):
273
    logger.info("*** Setup other catalogs ***")
274
275
    # contains tuples of (catalog, index) pairs
276
    to_reindex = []
277
278
    # allow add-ons to use this handler with other index/column definitions
279
    if indexes is None:
280
        indexes = INDEXES
281
    if columns is None:
282
        columns = COLUMNS
283
284
    # catalog indexes
285
    for catalog, idx_id, idx_attr, idx_type in indexes:
286
        catalog = api.get_tool(catalog)
287
        if add_catalog_index(catalog, idx_id, idx_attr, idx_type):
288
            to_reindex.append((catalog, idx_id))
289
        else:
290
            continue
291
292
    # catalog columns
293
    for catalog, column in columns:
294
        catalog = api.get_tool(catalog)
295
        add_catalog_column(catalog, column)
296
297
    # reindex new indexes
298
    for catalog, idx_id in to_reindex:
299
        reindex_catalog_index(catalog, idx_id)
300
301
302
def reindex_catalog_index(catalog, index):
303
    catalog_id = catalog.id
304
    logger.info("*** Indexing new index '%s' in '%s' ..."
305
                % (index, catalog_id))
306
    reindex_index(catalog, index)
307
    logger.info("*** Indexing new index '%s' in '%s' [DONE]"
308
                % (index, catalog_id))
309
310
311
def add_catalog_index(catalog, idx_id, idx_attr, idx_type):
312
    indexes = get_indexes(catalog)
313
    # check if the index exists
314
    if idx_id in indexes:
315
        logger.info("*** %s '%s' already in catalog '%s'"
316
                    % (idx_type, idx_id, catalog.id))
317
        return False
318
    # create the index
319
    add_index(catalog, idx_id, idx_type, indexed_attrs=idx_attr)
320
    logger.info("*** Added %s '%s' for catalog '%s'"
321
                % (idx_type, idx_id, catalog.id))
322
    return True
323
324
325
def add_catalog_column(catalog, column):
326
    columns = get_columns(catalog)
327
    if column in columns:
328
        logger.info("*** Column '%s' already in catalog '%s'"
329
                    % (column, catalog.id))
330
        return False
331
    add_column(catalog, column)
332
    logger.info("*** Added column '%s' to catalog '%s'"
333
                % (column, catalog.id))
334
    return True
335
336
337
def setup_catalog_mappings(portal, catalog_mappings=None):
338
    """Setup portal_type -> catalog mappings
339
    """
340
    logger.info("*** Setup Catalog Mappings ***")
341
342
    # allow add-ons to use this handler with own mappings
343
    if catalog_mappings is None:
344
        catalog_mappings = CATALOG_MAPPINGS
345
346
    at = api.get_tool("archetype_tool")
347
    for portal_type, catalogs in catalog_mappings:
348
        at.setCatalogsByType(portal_type, catalogs)
349
350
351
def setup_auditlog_catalog_mappings(portal):
352
    """Map auditlog catalog to all AT content types
353
    """
354
    at = api.get_tool("archetype_tool")
355
    pt = api.get_tool("portal_types")
356
    portal_types = pt.listContentTypes()
357
358
    # map all known types to the auditlog catalog
359
    auditlog_catalog = api.get_tool(AUDITLOG_CATALOG)
360
    for portal_type in portal_types:
361
        catalogs = at.getCatalogsByType(portal_type)
362
        if auditlog_catalog not in catalogs:
363
            existing_catalogs = list(map(lambda c: c.getId(), catalogs))
364
            new_catalogs = existing_catalogs + [AUDITLOG_CATALOG]
365
            at.setCatalogsByType(portal_type, new_catalogs)
366
            logger.info("*** Adding catalog '{}' for '{}'".format(
367
                AUDITLOG_CATALOG, portal_type))
368
369
370
def remove_default_content(portal):
371
    """Remove default Plone contents
372
    """
373
    logger.info("*** Remove Default Content ***")
374
375
    # Get the list of object ids for portal
376
    object_ids = portal.objectIds()
377
    delete_ids = filter(lambda id: id in object_ids, CONTENTS_TO_DELETE)
378
    if delete_ids:
379
        portal.manage_delObjects(ids=list(delete_ids))
380
381
382
def setup_content_structure(portal):
383
    """Install the base content structure
384
    """
385
    logger.info("*** Install SENAITE Content Types ***")
386
    _run_import_step(portal, "content")
387
    reindex_content_structure(portal)
388
389
390
def setup_form_controller_more_action(portal):
391
    """Install form controller actions for ported record widgets
392
393
    Code taken from Products.ATExtensions
394
    """
395
    logger.info("*** Install SENAITE Form Controller Actions ***")
396
    pfc = portal.portal_form_controller
397
    pfc.addFormValidators(
398
        "base_edit", "", "more", "")
399
    pfc.addFormAction(
400
        "base_edit", "success", "", "more", "traverse_to", "string:more_edit")
401
    pfc.addFormValidators(
402
        "atct_edit", "", "more", "",)
403
    pfc.addFormAction(
404
        "atct_edit", "success", "", "more", "traverse_to", "string:more_edit")
405
406
407
def _run_import_step(portal, name, profile="profile-bika.lims:default"):
408
    """Helper to install a GS import step from the given profile
409
    """
410
    logger.info("*** Running import step '{}' from profile '{}' ***"
411
                .format(name, profile))
412
    setup = portal.portal_setup
413
    setup.runImportStepFromProfile(profile, name)
414
415
416
def pre_install(portal_setup):
417
    """Runs berfore the first import step of the *default* profile
418
419
    This handler is registered as a *pre_handler* in the generic setup profile
420
421
    :param portal_setup: SetupTool
422
    """
423
    logger.info("SENAITE CORE pre-install handler [BEGIN]")
424
425
    # https://docs.plone.org/develop/addons/components/genericsetup.html#custom-installer-code-setuphandlers-py
426
    profile_id = PROFILE_ID
427
    context = portal_setup._getImportContext(profile_id)
428
    portal = context.getSite()  # noqa
429
430
    logger.info("SENAITE CORE pre-install handler [DONE]")
431
432
433
def post_install(portal_setup):
434
    """Runs after the last import step of the *default* profile
435
436
    This handler is registered as a *post_handler* in the generic setup profile
437
438
    :param portal_setup: SetupTool
439
    """
440
    logger.info("SENAITE CORE post install handler [BEGIN]")
441
442
    # https://docs.plone.org/develop/addons/components/genericsetup.html#custom-installer-code-setuphandlers-py
443
    profile_id = PROFILE_ID
444
    context = portal_setup._getImportContext(profile_id)
445
    portal = context.getSite()  # noqa
446
447
    # always apply the skins profile last to ensure our layers are first
448
    _run_import_step(portal, "skins", profile=profile_id)
449
450
    logger.info("SENAITE CORE post install handler [DONE]")
451
452
453
def setup_markup_schema(portal):
454
    """Sets the default and allowed markup schemas for RichText widgets
455
    """
456
    if not IMarkupSchema:
457
        return
458
459
    registry = getUtility(IRegistry, context=portal)
460
    settings = registry.forInterface(IMarkupSchema, prefix='plone')
461
    settings.default_type = u"text/html"
462
    settings.allowed_types = ("text/html", )
463