Passed
Push — 2.x ( 353ef7...3874f5 )
by Jordi
05:23
created

senaite.core.setuphandlers.install()   A

Complexity

Conditions 4

Size

Total Lines 48
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 31
dl 0
loc 48
rs 9.1359
c 0
b 0
f 0
cc 4
nop 1
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
    # Used in bika_setup, therefore it has to be added here
176
    add_senaite_setup(portal)
177
178
    # Run required import steps
179
    _run_import_step(portal, "skins")
180
    _run_import_step(portal, "browserlayer")
181
    _run_import_step(portal, "rolemap")
182
    _run_import_step(portal, "typeinfo")
183
    _run_import_step(portal, "factorytool")
184
    _run_import_step(portal, "workflow", "profile-senaite.core:default")
185
    _run_import_step(portal, "typeinfo", "profile-senaite.core:default")
186
187
    # skip installers if already installed
188
    qi = get_installer(portal)
189
    profiles = ["bika.lims", "senaite.core"]
190
    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...
191
        logger.info("SENAITE CORE already installed [SKIP]")
192
        return
193
194
    # Run Installers
195
    setup_groups(portal)
196
    remove_default_content(portal)
197
    # setup catalogs
198
    setup_core_catalogs(portal)
199
    setup_other_catalogs(portal)
200
    setup_catalog_mappings(portal)
201
    setup_auditlog_catalog_mappings(portal)
202
    setup_content_structure(portal)
203
    add_dexterity_portal_items(portal)
204
    add_dexterity_setup_items(portal)
205
206
    # Set CMF Form actions
207
    setup_form_controller_actions(portal)
208
    setup_form_controller_more_action(portal)
209
210
    # Setup markup default and allowed schemas
211
    setup_markup_schema(portal)
212
213
    logger.info("SENAITE CORE install handler [DONE]")
214
215
216
def add_dexterity_setup_items(portal):
217
    """Adds the Dexterity Container in the Setup Folder
218
219
    N.B.: We do this in code, because adding this as Generic Setup Profile in
220
          `profiles/default/structure` flushes the contents on every import.
221
    """
222
    # Tuples of ID, Title, FTI
223
    items = [
224
        ("dynamic_analysisspecs",  # ID
225
         "Dynamic Analysis Specifications",  # Title
226
         "DynamicAnalysisSpecs"),  # FTI
227
228
        ("interpretation_templates",
229
         "Interpretation Templates",
230
         "InterpretationTemplates"),
231
232
        ("sample_containers",
233
         "Sample Containers",
234
         "SampleContainers")
235
    ]
236
    setup = api.get_setup()
237
    add_dexterity_items(setup, items)
238
239
240
def add_senaite_setup(portal):
241
    """Add the new SENAITE Setup container
242
    """
243
    items = [
244
        # ID, Title, FTI
245
        ("setup", "SENAITE Setup", "Setup"),
246
    ]
247
    add_dexterity_items(portal, items)
248
249
    # Move Setup at the beginning
250
    portal.moveObjectToPosition("setup", 0)
251
252
    # Reindex order
253
    portal.plone_utils.reindexOnReorder(portal)
254
255
256
def add_dexterity_portal_items(portal):
257
    """Adds the Dexterity Container in the Site folder
258
259
    N.B.: We do this in code, because adding this as Generic Setup Profile in
260
          `profiles/default/structure` flushes the contents on every import.
261
    """
262
    # Tuples of ID, Title, FTI
263
    items = [
264
        # ID, Title, FTI
265
        ("samples", "Samples", "Samples"),
266
    ]
267
    add_dexterity_items(portal, items)
268
269
    # Move Samples after Clients nav item
270
    position = portal.getObjectPosition("clients")
271
    portal.moveObjectToPosition("samples", position + 1)
272
273
    # Reindex order
274
    portal.plone_utils.reindexOnReorder(portal)
275
276
277
def add_dexterity_items(container, items):
278
    """Adds a dexterity item, usually a folder in the container
279
    :param container: container of the items to add
280
    :param items: tuple of Id, Title, FTI
281
    """
282
    for id, title, fti in items:
283
        obj = container.get(id)
284
        if obj is None:
285
            with temporary_allow_type(container, fti) as ct:
286
                obj = api.create(ct, fti, id=id, title=title)
287
        else:
288
            obj.setTitle(title)
289
        obj.reindexObject()
290
291
292
def setup_core_catalogs(portal, catalog_classes=None, reindex=True):
293
    """Setup core catalogs
294
    """
295
    logger.info("*** Setup core catalogs ***")
296
    at = api.get_tool("archetype_tool")
297
298
    # allow add-ons to use this handler with own catalogs
299
    if catalog_classes is None:
300
        catalog_classes = CATALOGS
301
302
    # contains tuples of (catalog, index) pairs
303
    to_reindex = []
304
305
    for cls in catalog_classes:
306
        module = _resolveDottedName(cls.__module__)
307
308
        # get the required attributes from the module
309
        catalog_id = module.CATALOG_ID
310
        catalog_indexes = module.INDEXES
311
        catalog_columns = module.COLUMNS
312
        catalog_types = module.TYPES
313
314
        catalog = getattr(aq_base(portal), catalog_id, None)
315
        if catalog is None:
316
            catalog = cls()
317
            catalog._setId(catalog_id)
318
            portal._setObject(catalog_id, catalog)
319
320
        # catalog indexes
321
        for idx_id, idx_attr, idx_type in catalog_indexes:
322
            if add_catalog_index(catalog, idx_id, idx_attr, idx_type):
323
                to_reindex.append((catalog, idx_id))
324
            else:
325
                continue
326
327
        # catalog columns
328
        for column in catalog_columns:
329
            add_catalog_column(catalog, column)
330
331
        if not reindex:
332
            logger.info("*** Skipping reindex of new indexes")
333
            return
334
335
        # map allowed types to this catalog in archetype_tool
336
        for portal_type in catalog_types:
337
            # check existing catalogs
338
            catalogs = at.getCatalogsByType(portal_type)
339
            if catalog not in catalogs:
340
                existing = list(map(lambda c: c.getId(), catalogs))
341
                new_catalogs = existing + [catalog_id]
342
                at.setCatalogsByType(portal_type, new_catalogs)
343
                logger.info("*** Mapped catalog '%s' for type '%s'"
344
                            % (catalog_id, portal_type))
345
346
    # reindex new indexes
347
    for catalog, idx_id in to_reindex:
348
        reindex_catalog_index(catalog, idx_id)
349
350
351
def setup_other_catalogs(portal, indexes=None, columns=None):
352
    logger.info("*** Setup other catalogs ***")
353
354
    # contains tuples of (catalog, index) pairs
355
    to_reindex = []
356
357
    # allow add-ons to use this handler with other index/column definitions
358
    if indexes is None:
359
        indexes = INDEXES
360
    if columns is None:
361
        columns = COLUMNS
362
363
    # catalog indexes
364
    for catalog, idx_id, idx_attr, idx_type in indexes:
365
        catalog = api.get_tool(catalog)
366
        if add_catalog_index(catalog, idx_id, idx_attr, idx_type):
367
            to_reindex.append((catalog, idx_id))
368
        else:
369
            continue
370
371
    # catalog columns
372
    for catalog, column in columns:
373
        catalog = api.get_tool(catalog)
374
        add_catalog_column(catalog, column)
375
376
    # reindex new indexes
377
    for catalog, idx_id in to_reindex:
378
        reindex_catalog_index(catalog, idx_id)
379
380
381
def reindex_catalog_index(catalog, index):
382
    catalog_id = catalog.id
383
    logger.info("*** Indexing new index '%s' in '%s' ..."
384
                % (index, catalog_id))
385
    reindex_index(catalog, index)
386
    logger.info("*** Indexing new index '%s' in '%s' [DONE]"
387
                % (index, catalog_id))
388
389
390
def add_catalog_index(catalog, idx_id, idx_attr, idx_type):
391
    indexes = get_indexes(catalog)
392
    # check if the index exists
393
    if idx_id in indexes:
394
        logger.info("*** %s '%s' already in catalog '%s'"
395
                    % (idx_type, idx_id, catalog.id))
396
        return False
397
    # create the index
398
    add_index(catalog, idx_id, idx_type, indexed_attrs=idx_attr)
399
    logger.info("*** Added %s '%s' for catalog '%s'"
400
                % (idx_type, idx_id, catalog.id))
401
    return True
402
403
404
def add_catalog_column(catalog, column):
405
    columns = get_columns(catalog)
406
    if column in columns:
407
        logger.info("*** Column '%s' already in catalog '%s'"
408
                    % (column, catalog.id))
409
        return False
410
    add_column(catalog, column)
411
    logger.info("*** Added column '%s' to catalog '%s'"
412
                % (column, catalog.id))
413
    return True
414
415
416
def setup_catalog_mappings(portal, catalog_mappings=None):
417
    """Setup portal_type -> catalog mappings
418
    """
419
    logger.info("*** Setup Catalog Mappings ***")
420
421
    # allow add-ons to use this handler with own mappings
422
    if catalog_mappings is None:
423
        catalog_mappings = CATALOG_MAPPINGS
424
425
    at = api.get_tool("archetype_tool")
426
    for portal_type, catalogs in catalog_mappings:
427
        at.setCatalogsByType(portal_type, catalogs)
428
429
430
def setup_auditlog_catalog_mappings(portal):
431
    """Map auditlog catalog to all AT content types
432
    """
433
    at = api.get_tool("archetype_tool")
434
    pt = api.get_tool("portal_types")
435
    portal_types = pt.listContentTypes()
436
437
    # map all known AT types to the auditlog catalog
438
    auditlog_catalog = api.get_tool(AUDITLOG_CATALOG)
439
    for portal_type in portal_types:
440
441
        # Do not map DX types into archetypes tool
442
        fti = pt.getTypeInfo(portal_type)
443
        if isinstance(fti, DexterityFTI):
444
            continue
445
446
        catalogs = at.getCatalogsByType(portal_type)
447
        if auditlog_catalog not in catalogs:
448
            existing_catalogs = list(map(lambda c: c.getId(), catalogs))
449
            new_catalogs = existing_catalogs + [AUDITLOG_CATALOG]
450
            at.setCatalogsByType(portal_type, new_catalogs)
451
            logger.info("*** Adding catalog '{}' for '{}'".format(
452
                AUDITLOG_CATALOG, portal_type))
453
454
455
def remove_default_content(portal):
456
    """Remove default Plone contents
457
    """
458
    logger.info("*** Remove Default Content ***")
459
460
    # Get the list of object ids for portal
461
    object_ids = portal.objectIds()
462
    delete_ids = filter(lambda id: id in object_ids, CONTENTS_TO_DELETE)
463
    if delete_ids:
464
        portal.manage_delObjects(ids=list(delete_ids))
465
466
467
def setup_content_structure(portal):
468
    """Install the base content structure
469
    """
470
    logger.info("*** Install SENAITE Content Types ***")
471
    _run_import_step(portal, "content")
472
    reindex_content_structure(portal)
473
474
475
def setup_form_controller_more_action(portal):
476
    """Install form controller actions for ported record widgets
477
478
    Code taken from Products.ATExtensions
479
    """
480
    logger.info("*** Install SENAITE Form Controller Actions ***")
481
    pfc = portal.portal_form_controller
482
    pfc.addFormValidators(
483
        "base_edit", "", "more", "")
484
    pfc.addFormAction(
485
        "base_edit", "success", "", "more", "traverse_to", "string:more_edit")
486
    pfc.addFormValidators(
487
        "atct_edit", "", "more", "",)
488
    pfc.addFormAction(
489
        "atct_edit", "success", "", "more", "traverse_to", "string:more_edit")
490
491
492
def _run_import_step(portal, name, profile="profile-bika.lims:default"):
493
    """Helper to install a GS import step from the given profile
494
    """
495
    logger.info("*** Running import step '{}' from profile '{}' ***"
496
                .format(name, profile))
497
    setup = portal.portal_setup
498
    setup.runImportStepFromProfile(profile, name)
499
500
501
def pre_install(portal_setup):
502
    """Runs berfore the first import step of the *default* profile
503
504
    This handler is registered as a *pre_handler* in the generic setup profile
505
506
    :param portal_setup: SetupTool
507
    """
508
    logger.info("SENAITE CORE pre-install handler [BEGIN]")
509
510
    # https://docs.plone.org/develop/addons/components/genericsetup.html#custom-installer-code-setuphandlers-py
511
    profile_id = PROFILE_ID
512
    context = portal_setup._getImportContext(profile_id)
513
    portal = context.getSite()  # noqa
514
515
    logger.info("SENAITE CORE pre-install handler [DONE]")
516
517
518
def post_install(portal_setup):
519
    """Runs after the last import step of the *default* profile
520
521
    This handler is registered as a *post_handler* in the generic setup profile
522
523
    :param portal_setup: SetupTool
524
    """
525
    logger.info("SENAITE CORE post install handler [BEGIN]")
526
527
    # https://docs.plone.org/develop/addons/components/genericsetup.html#custom-installer-code-setuphandlers-py
528
    profile_id = PROFILE_ID
529
    context = portal_setup._getImportContext(profile_id)
530
    portal = context.getSite()  # noqa
531
532
    # always apply the skins profile last to ensure our layers are first
533
    _run_import_step(portal, "skins", profile=profile_id)
534
535
    logger.info("SENAITE CORE post install handler [DONE]")
536
537
538
def setup_markup_schema(portal):
539
    """Sets the default and allowed markup schemas for RichText widgets
540
    """
541
    if not IMarkupSchema:
542
        return
543
544
    registry = getUtility(IRegistry, context=portal)
545
    settings = registry.forInterface(IMarkupSchema, prefix='plone')
546
    settings.default_type = u"text/html"
547
    settings.allowed_types = ("text/html", )
548