Passed
Push — master ( 804f45...1fa2c0 )
by Jordi
04:08
created

build.bika.lims.catalog.catalog_utilities   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 443
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 58
eloc 211
dl 0
loc 443
rs 4.5599
c 0
b 0
f 0

12 Functions

Rating   Name   Duplication   Size   Complexity  
A _delColumn() 0 19 3
A getCatalog() 0 24 3
D _merge_catalog_definitions() 0 62 12
A _addColumn() 0 18 3
B setup_catalogs() 0 60 7
A _addIndex() 0 22 4
A getCatalogDefinitions() 0 17 1
A _delIndex() 0 19 3
C _setup_catalog() 0 52 10
B _map_content_types() 0 46 5
A addZCTextIndex() 0 28 3
A _cleanAndRebuildIfNeeded() 0 15 4

How to fix   Complexity   

Complexity

Complex classes like build.bika.lims.catalog.catalog_utilities 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-2019 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from bika.lims import logger
22
from bika.lims.catalog.analysis_catalog import \
23
    bika_catalog_analysis_listing_definition
24
from bika.lims.catalog.analysisrequest_catalog import \
25
    bika_catalog_analysisrequest_listing_definition
26
from bika.lims.catalog.autoimportlogs_catalog import \
27
    bika_catalog_autoimportlogs_listing_definition
28
from bika.lims.catalog.report_catalog import bika_catalog_report_definition
29
from bika.lims.catalog.worksheet_catalog import \
30
    bika_catalog_worksheet_listing_definition
31
from Products.CMFCore.utils import getToolByName
32
33
34
def getCatalogDefinitions():
35
    """
36
    Returns a dictionary with catalogs definitions.
37
    """
38
    final = {}
39
    analysis_request = bika_catalog_analysisrequest_listing_definition
40
    analysis = bika_catalog_analysis_listing_definition
41
    autoimportlogs = bika_catalog_autoimportlogs_listing_definition
42
    worksheet = bika_catalog_worksheet_listing_definition
43
    report = bika_catalog_report_definition
44
    # Merging the catalogs
45
    final.update(analysis_request)
46
    final.update(analysis)
47
    final.update(autoimportlogs)
48
    final.update(worksheet)
49
    final.update(report)
50
    return final
51
52
53
def getCatalog(instance, field='UID'):
54
    """
55
    Returns the catalog that stores objects of instance passed in type.
56
    If an object is indexed by more than one catalog, the first match
57
    will be returned.
58
59
    :param instance: A single object
60
    :type instance: ATContentType
61
    :returns: The first catalog that stores the type of object passed in
62
    """
63
    uid = instance.UID()
64
    if 'workflow_skiplist' in instance.REQUEST and \
65
        [x for x in instance.REQUEST['workflow_skiplist']
66
         if x.find(uid) > -1]:
67
        return None
68
    else:
69
        # grab the first catalog we are indexed in.
70
        # we're only indexed in one.
71
        at = getToolByName(instance, 'archetype_tool')
72
        plone = instance.portal_url.getPortalObject()
73
        catalog_name = instance.portal_type in at.catalog_map \
74
            and at.catalog_map[instance.portal_type][0] or 'portal_catalog'
75
        catalog = getToolByName(plone, catalog_name)
76
        return catalog
77
78
def setup_catalogs(
79
        portal, catalogs_definition={},
80
        force_reindex=False, catalogs_extension={}, force_no_reindex=False):
81
    """
82
    Setup the given catalogs. Redefines the map between content types and
83
    catalogs and then checks the indexes and metacolumns, if one index/column
84
    doesn't exist in the catalog_definition any more it will be
85
    removed, otherwise, if a new index/column is found, it will be created.
86
87
    :param portal: The Plone's Portal object
88
    :param catalogs_definition: a dictionary with the following structure
89
        {
90
            CATALOG_ID: {
91
                'types':   ['ContentType', ...],
92
                'indexes': {
93
                    'UID': 'FieldIndex',
94
                    ...
95
                },
96
                'columns': [
97
                    'Title',
98
                    ...
99
                ]
100
            }
101
        }
102
    :type catalogs_definition: dict
103
    :param force_reindex: Force to reindex the catalogs even if there's no need
104
    :type force_reindex: bool
105
    :param force_no_reindex: Force reindexing NOT to happen.
106
    :param catalog_extensions: An extension for the primary catalogs definition
107
        Same dict structure as param catalogs_definition. Allows to add
108
        columns and indexes required by Bika-specific add-ons.
109
    :type catalog_extensions: dict
110
    """
111
    # If not given catalogs_definition, use the LIMS one
112
    if not catalogs_definition:
113
        catalogs_definition = getCatalogDefinitions()
114
115
    # Merge the catalogs definition of the extension with the primary
116
    # catalog definition
117
    definition = _merge_catalog_definitions(catalogs_definition,
118
                                            catalogs_extension)
119
120
    # Mapping content types in catalogs
121
    # This variable will be used to clean reindex the catalog. Saves the
122
    # catalogs ids
123
    archetype_tool = getToolByName(portal, 'archetype_tool')
124
    clean_and_rebuild = _map_content_types(archetype_tool, definition)
125
126
    # Indexing
127
    for cat_id in definition.keys():
128
        reindex = False
129
        reindex = _setup_catalog(
130
            portal, cat_id, definition.get(cat_id, {}))
131
        if (reindex or force_reindex) and (cat_id not in clean_and_rebuild):
132
            # add the catalog if it has not been added before
133
            clean_and_rebuild.append(cat_id)
134
    # Reindex the catalogs which needs it
135
    if not force_no_reindex:
136
        _cleanAndRebuildIfNeeded(portal, clean_and_rebuild)
137
    return clean_and_rebuild
138
139
def _merge_catalog_definitions(dict1, dict2):
140
    """
141
    Merges two dictionaries that represent catalogs definitions. The first
142
    dictionary contains the catalogs structure by default and the second dict
143
    contains additional information. Usually, the former is the Bika LIMS
144
    catalogs definition and the latter is the catalogs definition of an add-on
145
    The structure of each dict as follows:
146
        {
147
            CATALOG_ID: {
148
                'types':   ['ContentType', ...],
149
                'indexes': {
150
                    'UID': 'FieldIndex',
151
                    ...
152
                },
153
                'columns': [
154
                    'Title',
155
                    ...
156
                ]
157
            }
158
        }
159
160
    :param dict1: The dictionary to be used as the main template (defaults)
161
    :type dict1: dict
162
    :param dict2: The dictionary with additional information
163
    :type dict2: dict
164
    :returns: A merged dict with the same structure as the dicts passed in
165
    :rtype: dict
166
    """
167
    if not dict2:
168
        return dict1.copy()
169
170
    outdict = {}
171
    # Use dict1 as a template
172
    for k, v in dict1.items():
173
        if k not in dict2 and isinstance(v, dict):
174
            outdict[k] = v.copy()
175
            continue
176
        if k not in dict2 and isinstance(v, list):
177
            outdict[k] = v[:]
178
            continue
179
        if k == 'indexes':
180
            sdict1 = v.copy()
181
            sdict2 = dict2[k].copy()
182
            sdict1.update(sdict2)
183
            outdict[k] = sdict1
184
            continue
185
        if k in ['types', 'columns']:
186
            list1 = v
187
            list2 = dict2[k]
188
            outdict[k] = list(set(list1 + list2))
189
            continue
190
        if isinstance(v, dict):
191
            sdict1 = v.copy()
192
            sdict2 = dict2[k].copy()
193
            outdict[k] = _merge_catalog_definitions(sdict1, sdict2)
194
195
    # Now, add the rest of keys from dict2 that don't exist in dict1
196
    for k, v in dict2.items():
197
        if k in outdict:
198
            continue
199
        outdict[k] = v.copy()
200
    return outdict
201
202
def _map_content_types(archetype_tool, catalogs_definition):
203
    """
204
    Updates the mapping for content_types against catalogs
205
    :archetype_tool: an archetype_tool object
206
    :catalogs_definition: a dictionary like
207
        {
208
            CATALOG_ID: {
209
                'types':   ['ContentType', ...],
210
                'indexes': {
211
                    'UID': 'FieldIndex',
212
                    ...
213
                },
214
                'columns': [
215
                    'Title',
216
                    ...
217
                ]
218
            }
219
        }
220
    """
221
    # This will be a dictionari like {'content_type':['catalog_id', ...]}
222
    ct_map = {}
223
    # This list will contain the atalog ids to be rebuild
224
    to_reindex = []
225
    # getting the dictionary of mapped content_types in the catalog
226
    map_types = archetype_tool.catalog_map
227
    for catalog_id in catalogs_definition.keys():
228
        catalog_info = catalogs_definition.get(catalog_id, {})
229
        # Mapping the catalog with the defined types
230
        types = catalog_info.get('types', [])
231
        for t in types:
232
            tmp_l = ct_map.get(t, [])
233
            tmp_l.append(catalog_id)
234
            ct_map[t] = tmp_l
235
    # Mapping
236
    for t in ct_map.keys():
237
        catalogs_list = ct_map[t]
238
        # Getting the previus mapping
239
        perv_catalogs_list = archetype_tool.catalog_map.get(t, [])
240
        # If the mapping has changed, update it
241
        set1 = set(catalogs_list)
242
        set2 = set(perv_catalogs_list)
243
        if set1 != set2:
244
            archetype_tool.setCatalogsByType(t, catalogs_list)
245
            # Adding to reindex only the catalogs that have changed
246
            to_reindex = to_reindex + list(set1 - set2) + list(set2 - set1)
247
    return to_reindex
248
249
250
def _setup_catalog(portal, catalog_id, catalog_definition):
251
    """
252
    Given a catalog definition it updates the indexes, columns and content_type
253
    definitions of the catalog.
254
    :portal: the Plone site object
255
    :catalog_id: a string as the catalog id
256
    :catalog_definition: a dictionary like
257
        {
258
            'types':   ['ContentType', ...],
259
            'indexes': {
260
                'UID': 'FieldIndex',
261
                ...
262
            },
263
            'columns': [
264
                'Title',
265
                ...
266
            ]
267
        }
268
    """
269
270
    reindex = False
271
    catalog = getToolByName(portal, catalog_id, None)
272
    if catalog is None:
273
        logger.warning('Could not find the %s tool.' % (catalog_id))
274
        return False
275
    # Indexes
276
    indexes_ids = catalog_definition.get('indexes', {}).keys()
277
    # Indexing
278
    for idx in indexes_ids:
279
        # The function returns if the index needs to be reindexed
280
        indexed = _addIndex(catalog, idx, catalog_definition['indexes'][idx])
281
        reindex = True if indexed else reindex
282
    # Removing indexes
283
    in_catalog_idxs = catalog.indexes()
284
    to_remove = list(set(in_catalog_idxs)-set(indexes_ids))
285
    for idx in to_remove:
286
        # The function returns if the index has been deleted
287
        desindexed = _delIndex(catalog, idx)
288
        reindex = True if desindexed else reindex
289
    # Columns
290
    columns_ids = catalog_definition.get('columns', [])
291
    for col in columns_ids:
292
        created = _addColumn(catalog, col)
293
        reindex = True if created else reindex
294
    # Removing columns
295
    in_catalog_cols = catalog.schema()
296
    to_remove = list(set(in_catalog_cols)-set(columns_ids))
297
    for col in to_remove:
298
        # The function returns if the index has been deleted
299
        desindexed = _delColumn(catalog, col)
300
        reindex = True if desindexed else reindex
301
    return reindex
302
303
304
def _addIndex(catalog, index, indextype):
305
    """
306
    This function indexes the index element into the catalog if it isn't yet.
307
    :catalog: a catalog object
308
    :index: an index id as string
309
    :indextype: the type of the index as string
310
    :returns: a boolean as True if the element has been indexed and it returns
311
    False otherwise.
312
    """
313
    if index not in catalog.indexes():
314
        try:
315
            if indextype == 'ZCTextIndex':
316
                addZCTextIndex(catalog, index)
317
            else:
318
                catalog.addIndex(index, indextype)
319
            logger.info('Catalog index %s added to %s.' % (index, catalog.id))
320
            return True
321
        except:
322
            logger.error(
323
                'Catalog index %s error while adding to %s.'
324
                % (index, catalog.id))
325
    return False
326
327
328
def _addColumn(cat, col):
329
    """
330
    This function adds a metadata column to the acatalog.
331
    :cat: a catalog object
332
    :col: a column id as string
333
    :returns: a boolean as True if the element has been added and
334
        False otherwise
335
    """
336
    # First check if the metadata column already exists
337
    if col not in cat.schema():
338
        try:
339
            cat.addColumn(col)
340
            logger.info('Column %s added to %s.' % (col, cat.id))
341
            return True
342
        except:
343
            logger.error(
344
                'Catalog column %s error while adding to %s.' % (col, cat.id))
345
    return False
346
347
348
def _delIndex(catalog, index):
349
    """
350
    This function desindexes the index element from the catalog.
351
    :catalog: a catalog object
352
    :index: an index id as string
353
    :returns: a boolean as True if the element has been desindexed and it
354
    returns False otherwise.
355
    """
356
    if index in catalog.indexes():
357
        try:
358
            catalog.delIndex(index)
359
            logger.info(
360
                'Catalog index %s deleted from %s.' % (index, catalog.id))
361
            return True
362
        except:
363
            logger.error(
364
                'Catalog index %s error while deleting from %s.'
365
                % (index, catalog.id))
366
    return False
367
368
369
def _delColumn(cat, col):
370
    """
371
    This function deletes a metadata column of the acatalog.
372
    :cat: a catalog object
373
    :col: a column id as string
374
    :returns: a boolean as True if the element has been removed and
375
        False otherwise
376
    """
377
    # First check if the metadata column already exists
378
    if col in cat.schema():
379
        try:
380
            cat.delColumn(col)
381
            logger.info('Column %s deleted from %s.' % (col, cat.id))
382
            return True
383
        except:
384
            logger.error(
385
                'Catalog column %s error while deleting from %s.'
386
                % (col, cat.id))
387
    return False
388
389
390
def addZCTextIndex(catalog, index_name):
391
392
    if catalog is None:
393
        logger.warning('Could not find the catalog tool.' + catalog)
394
        return
395
396
    # Create lexicon to be able to add ZCTextIndex
397
    wordSplitter = Empty()
398
    wordSplitter.group = 'Word Splitter'
399
    wordSplitter.name = 'Unicode Whitespace splitter'
400
    caseNormalizer = Empty()
401
    caseNormalizer.group = 'Case Normalizer'
402
    caseNormalizer.name = 'Unicode Case Normalizer'
403
    stopWords = Empty()
404
    stopWords.group = 'Stop Words'
405
    stopWords.name = 'Remove listed and single char words'
406
    elem = [wordSplitter, caseNormalizer, stopWords]
407
    zc_extras = Empty()
408
    zc_extras.index_type = 'Okapi BM25 Rank'
409
    zc_extras.lexicon_id = 'Lexicon'
410
411
    try:
412
        catalog.manage_addProduct['ZCTextIndex'].manage_addLexicon('Lexicon',
413
                                                               'Lexicon', elem)
414
    except:
415
        logger.warning('Could not add ZCTextIndex to '+str(catalog))
416
417
    catalog.addIndex(index_name, 'ZCTextIndex', zc_extras)
418
419
420
def _cleanAndRebuildIfNeeded(portal, cleanrebuild):
421
    """
422
    Rebuild the given catalogs.
423
    :portal: the Plone portal object
424
    :cleanrebuild: a list with catalog ids
425
    """
426
    for cat in cleanrebuild:
427
        catalog = getToolByName(portal, cat)
428
        if catalog:
429
            if hasattr(catalog, "softClearFindAndRebuild"):
430
                catalog.softClearFindAndRebuild()
431
            else:
432
                catalog.clearFindAndRebuild()
433
        else:
434
            logger.warning('%s do not found' % cat)
435
436
437
class Empty:
438
    """
439
    Just a class to use when we need an object with some attributes to send to
440
    another objects an a parameter.
441
    """
442
    pass
443