Passed
Push — 2.x ( 4e3b0a...8119a4 )
by Jordi
07:03
created

senaite.core.content.worksheettemplate   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 555
Duplicated Lines 8.47 %

Importance

Changes 0
Metric Value
wmc 39
eloc 366
dl 47
loc 555
rs 9.28
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A WorksheetTemplate.setTemplateLayout() 0 7 2
A WorksheetTemplate.getRawRestrictToMethod() 0 6 2
A WorksheetTemplate.get_instrument_query() 12 26 4
B WorksheetTemplate.setServices() 35 35 6
A WorksheetTemplate.getInstrument() 0 4 1
A WorksheetTemplate.getTemplateLayout() 0 4 1
A WorksheetTemplate.getServices() 0 11 1
A WorksheetTemplate.setRestrictToMethod() 0 4 1
A WorksheetTemplate.getRestrictToMethod() 0 4 1
B WorksheetTemplate.setNumOfPositions() 0 23 7
A WorksheetTemplate.getRawService() 0 5 1
A WorksheetTemplate.getNumOfPositions() 0 4 1
A WorksheetTemplate.getRawInstrument() 0 6 2
A WorksheetTemplate.setInstrument() 0 4 1
A WorksheetTemplate.getRawServices() 0 14 2

2 Functions

Rating   Name   Duplication   Size   Complexity  
A get_query_num_positions() 0 7 3
A default_layout_positions() 0 24 3

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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-2024 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from AccessControl import ClassSecurityInfo
22
from Products.CMFCore import permissions
23
from bika.lims import api
24
from bika.lims import deprecated
25
from bika.lims import senaiteMessageFactory as _
26
from bika.lims.interfaces import IDeactivable
27
from plone.supermodel import model
28
from plone.autoform import directives
29
from senaite.core.catalog import SETUP_CATALOG
30
from senaite.core.schema import UIDReferenceField
31
from senaite.core.schema.fields import DataGridRow
32
from senaite.core.content.base import Container
33
from senaite.core.config.widgets import get_default_columns
34
from senaite.core.interfaces import IWorksheetTemplate
35
from senaite.core.z3cform.widgets.number import NumberWidget
36
from senaite.core.z3cform.widgets.datagrid import DataGridWidgetFactory
37
from senaite.core.z3cform.widgets.uidreference import UIDReferenceWidgetFactory
38
from senaite.core.z3cform.widgets.listing.widget import ListingWidgetFactory
39
from zope import schema
40
from zope.interface import Interface
41
from zope.interface import implementer
42
from zope.schema.vocabulary import SimpleVocabulary
43
from z3c.form.interfaces import DISPLAY_MODE
44
from six.moves.urllib.parse import parse_qs
45
46
47
def get_query_num_positions():
48
    request = api.get_request()
49
    if request:
50
        params = dict(parse_qs(request["QUERY_STRING"]))
51
        values = params.get("num_positions")
52
        return int(values[0]) if values else 0
53
    return 0
54
55
56
def default_layout_positions(count_positions=None, start_pos=0):
57
    """Generate preset for Template Layout for new WorksheetTemplate with query
58
    string parameter 'num_positions' or generate preset from edit form
59
60
    :param count_positions: number of positions for Worksheet
61
    :param start_pos: Index for starting first position
62
    :returns: Array of object for description layout of worksheet
63
    """
64
    num_positions = get_query_num_positions()
65
    if count_positions:
66
        num_positions = int(count_positions)
67
68
    default_value = []
69
    for i in range(num_positions):
70
        default_value.append({
71
            "pos": start_pos + i + 1,
72
            "type": "a",
73
            "blank_ref": [],
74
            "control_ref": [],
75
            "reference_proxy": None,
76
            "dup_proxy": None,
77
            "dup": None,
78
        })
79
    return default_value
80
81
82
class IWorksheetTemplateServiceRecord(Interface):
83
    """Record schema for selected services
84
    """
85
    uid = schema.TextLine(
86
        title=_(u"title_service_uid", default=u"Service UID")
87
    )
88
89
90
class ILayoutRecord(Interface):
91
    """Record schema for layout worksheet
92
    """
93
94
    directives.widget("pos", klass="field")
95
    pos = schema.Int(
96
        title=_(
97
            u"title_layout_record_pos",
98
            default=u"Position"
99
        ),
100
        required=True,
101
        default=1,
102
    )
103
104
    directives.widget("type", klass="field")
105
    type = schema.Choice(
106
        title=_(
107
            u"title_layout_record_type",
108
            default=u"Analysis Type"
109
        ),
110
        vocabulary="senaite.core.vocabularies.analysis_types",
111
        required=True,
112
        default=u"a",
113
    )
114
115
    directives.widget(
116
        "blank_ref",
117
        UIDReferenceWidgetFactory,
118
        catalog=SETUP_CATALOG,
119
        query={
120
            "portal_type": "ReferenceDefinition",
121
            "is_active": True,
122
            "sort_limit": 5,
123
            "sort_on": "sortable_title",
124
            "sort_order": "ascending",
125
        },
126
        display_template="<a href='${url}'>${title}</a>",
127
        columns=get_default_columns,
128
        limit=5,
129
    )
130
    directives.mode(blank_ref="hidden")
131
    blank_ref = UIDReferenceField(
132
        title=_(
133
            u"title_layout_record_blank_ref",
134
            default=u"Blank Reference"
135
        ),
136
        allowed_types=("ReferenceDefinition",),
137
        multi_valued=False,
138
        required=False,
139
    )
140
141
    directives.widget(
142
        "control_ref",
143
        UIDReferenceWidgetFactory,
144
        catalog=SETUP_CATALOG,
145
        query={
146
            "portal_type": "ReferenceDefinition",
147
            "is_active": True,
148
            "sort_limit": 5,
149
            "sort_on": "sortable_title",
150
            "sort_order": "ascending",
151
        },
152
        display_template="<a href='${url}'>${title}</a>",
153
        columns=get_default_columns,
154
        limit=5,
155
    )
156
    directives.mode(control_ref="hidden")
157
    control_ref = UIDReferenceField(
158
        title=_(
159
            u"title_layout_record_control_ref",
160
            default=u"Control Reference"
161
        ),
162
        allowed_types=("ReferenceDefinition",),
163
        multi_valued=False,
164
        required=False,
165
    )
166
167
    directives.widget("reference_proxy", klass="field")
168
    reference_proxy = schema.Choice(
169
        title=_(
170
            u"title_layout_record_reference_proxy",
171
            default=u"Reference"
172
        ),
173
        vocabulary="senaite.core.vocabularies.reference_definition",
174
        required=False,
175
        default=None,
176
    )
177
178
    directives.widget("dup_proxy", klass="field")
179
    dup_proxy = schema.Choice(
180
        title=_(
181
            u"title_layout_record_dup_proxy",
182
            default=u"Duplicate Of"
183
        ),
184
        source=SimpleVocabulary.fromValues([0]),
185
        required=False,
186
        default=None,
187
    )
188
189
    directives.mode(dup="hidden")
190
    dup = schema.Int(
191
        title=_(
192
            u"title_layout_record_dup",
193
            default=u"Duplicate Of"
194
        ),
195
        required=False,
196
        default=None,
197
    )
198
199
200
class IWorksheetTemplateSchema(model.Schema):
201
    """Schema interface
202
    """
203
204
    title = schema.TextLine(
205
        title=_(
206
            u"title_worksheettemplate_title",
207
            default=u"Name"
208
        ),
209
        required=True,
210
    )
211
212
    description = schema.Text(
213
        title=_(
214
            u"title_worksheettemplate_description",
215
            default=u"Description"
216
        ),
217
        required=False,
218
    )
219
220
    directives.widget(
221
        "restrict_to_method",
222
        UIDReferenceWidgetFactory,
223
        catalog=SETUP_CATALOG,
224
        query={
225
            "portal_type": "Method",
226
            "is_active": True,
227
            "sort_limit": 5,
228
            "sort_on": "sortable_title",
229
            "sort_order": "ascending",
230
        },
231
        display_template="<a href='${url}'>${title}</a>",
232
        columns=get_default_columns,
233
        limit=5,
234
    )
235
    restrict_to_method = UIDReferenceField(
236
        title=_(
237
            u"label_worksheettemplate_restrict_to_method",
238
            default=u"Restrict to Method"
239
        ),
240
        description=_(
241
            u"description_worksheettemplate_restrict_to_method",
242
            default=u"Restrict the available analysis services and "
243
                    u"instruments to those with the selected method.<br/>"
244
                    u"In order to apply this change to the services list, "
245
                    u"you should save the change first."
246
        ),
247
        allowed_types=("Method",),
248
        multi_valued=False,
249
        required=False,
250
    )
251
252
    directives.widget(
253
        "instrument",
254
        UIDReferenceWidgetFactory,
255
        catalog=SETUP_CATALOG,
256
        query="get_instrument_query",
257
        display_template="<a href='${url}'>${title}</a>",
258
        columns=get_default_columns,
259
        limit=5,
260
    )
261
    instrument = UIDReferenceField(
262
        title=_(
263
            u"label_worksheettemplate_instrument",
264
            default=u"Preferred instrument"
265
        ),
266
        description=_(u"description_worksheettemplate_instrument",
267
                      default=u"Select the preferred instrument"),
268
        allowed_types=("Instrument",),
269
        multi_valued=False,
270
        required=False,
271
    )
272
273
    model.fieldset(
274
        "layout",
275
        label=_(
276
            u"label_fieldset_worksheettemplate_layout",
277
            default=u"Layout"
278
        ),
279
        fields=[
280
            "num_of_positions",
281
            "template_layout",
282
        ]
283
    )
284
285
    directives.widget(
286
        "num_of_positions",
287
        NumberWidget,
288
        klass="num-positions")
289
    num_of_positions = schema.Int(
290
        title=_(
291
            u"title_worksheettemplate_num_of_positions",
292
            default=u"Number of Positions"
293
        ),
294
        required=False,
295
        defaultFactory=get_query_num_positions,
296
        min=0,
297
        default=0,
298
    )
299
300
    directives.widget(
301
        "template_layout",
302
        DataGridWidgetFactory,
303
        allow_insert=False,
304
        allow_delete=False,
305
        allow_reorder=False,
306
        auto_append=False,
307
        templates = {
308
            DISPLAY_MODE: "ws_template_datagrid_display.pt",
309
        })
310
    template_layout = schema.List(
311
        title=_(
312
            u"title_worksheettemplate_template_layout",
313
            default=u"Worksheet Layout"
314
        ),
315
        description=_(
316
            u"description_worksheettemplate_template_layout",
317
            default=u"Specify the size of the Worksheet, e.g. corresponding "
318
                    u"to a specific instrument's tray size. "
319
                    u"Then select an Analysis 'type' per Worksheet position. "
320
                    u"Where QC samples are selected, also select which "
321
                    u"Reference Sample should be used. "
322
                    u"If a duplicate analysis is selected, indicate which "
323
                    u"sample position it should be a duplicate of"
324
        ),
325
        value_type=DataGridRow(schema=ILayoutRecord),
326
        defaultFactory=default_layout_positions,
327
        required=True,
328
    )
329
330
    model.fieldset(
331
        "analyses",
332
        label=_(u"label_fieldset_worksheettemplate_analyses",
333
                default=u"Analyses"),
334
        fields=[
335
            "services",
336
        ]
337
    )
338
339
    # Services
340
    directives.widget(
341
        "services",
342
        ListingWidgetFactory,
343
        listing_view="worksheettemplate_services_widget"
344
    )
345
    services = schema.List(
346
        title=_(
347
            u"title_worksheettemplate_services",
348
            default=u"Analysis Services"
349
        ),
350
        description=_(
351
            u"description_worksheettemplate_services",
352
            default=u"Select which Analyses should be included on the "
353
                    u"Worksheet"
354
        ),
355
        value_type=DataGridRow(schema=IWorksheetTemplateServiceRecord),
356
        default=[],
357
        required=False,
358
    )
359
360
361
@implementer(IWorksheetTemplate, IWorksheetTemplateSchema, IDeactivable)
362
class WorksheetTemplate(Container):
363
    """Worksheet Template type
364
    """
365
    # Catalogs where this type will be catalogued
366
    _catalogs = [SETUP_CATALOG]
367
368
    security = ClassSecurityInfo()
369
370
    @security.protected(permissions.View)
371
    def getRawRestrictToMethod(self):
372
        method = self.getRestrictToMethod()
373
        if method:
374
            return method.UID()
375
        return None
376
377
    @security.protected(permissions.View)
378
    def getRestrictToMethod(self):
379
        accessor = self.accessor("restrict_to_method")
380
        return accessor(self)
381
382
    @security.protected(permissions.ModifyPortalContent)
383
    def setRestrictToMethod(self, value):
384
        mutator = self.mutator("restrict_to_method")
385
        mutator(self, value)
386
387
    # BBB: AT schema field property
388
    RestrictToMethod = property(getRestrictToMethod, setRestrictToMethod)
389
390
    @security.protected(permissions.View)
391
    def getRawInstrument(self):
392
        instrument = self.getInstrument()
393
        if instrument:
394
            return instrument.UID()
395
        return None
396
397
    @security.protected(permissions.View)
398
    def getInstrument(self):
399
        accessor = self.accessor("instrument")
400
        return accessor(self)
401
402
    @security.protected(permissions.ModifyPortalContent)
403
    def setInstrument(self, value):
404
        mutator = self.mutator("instrument")
405
        mutator(self, value)
406
407
    # BBB: AT schema field property
408
    Instrument = property(getInstrument, setInstrument)
409
410
    @security.protected(permissions.View)
411
    def getNumOfPositions(self):
412
        accessor = self.accessor("num_of_positions")
413
        return accessor(self)
414
415
    @security.protected(permissions.ModifyPortalContent)
416
    def setNumOfPositions(self, value):
417
        if value == "":
418
            value = 0
419
        nums = int(value)
420
        mutator = self.mutator("num_of_positions")
421
        mutator(self, nums)
422
        layout = self.getTemplateLayout()
423
        len_layout = len(layout)
424
        if nums == 0:
425
            layout = []
426
        elif len_layout == 0 and nums > 0:
427
            # create default layout of 'nums' rows
428
            layout = default_layout_positions(nums)
429
        elif nums > len_layout:
430
            rows = nums - len_layout
431
            # added count 'rows' rows for layout
432
            # starting from index 'len_layout'
433
            layout = layout + default_layout_positions(rows, len_layout)
434
        elif nums < len_layout:
435
            # save firsts of 'nums' rows
436
            layout = layout[:nums]
437
        self.setTemplateLayout(layout)
438
439
    NumOfPositions = property(getNumOfPositions, setNumOfPositions)
440
441
    @security.protected(permissions.View)
442
    def getTemplateLayout(self):
443
        accessor = self.accessor("template_layout")
444
        return accessor(self) or []
445
446
    @security.protected(permissions.ModifyPortalContent)
447
    def setTemplateLayout(self, value):
448
        mutator = self.mutator("template_layout")
449
        mutator(self, value)
450
        num_pos = len(value)
451
        if num_pos != self.getNumOfPositions():
452
            self.setNumOfPositions(num_pos)
453
454
    # BBB: AT schema field property
455
    Layout = property(getTemplateLayout, setTemplateLayout)
456
457
    @security.protected(permissions.View)
458
    def getRawServices(self):
459
        """Return the raw value of the services field
460
461
        >>> self.getRawServices()
462
        ['<uid>', ...]
463
464
        :returns: List of uid's
465
        """
466
        accessor = self.accessor("services")
467
        services = accessor(self)
468
        if services:
469
            return [s.get("uid") for s in services]
470
        return []
471
472
    @deprecated(comment="If you need to get the service, use getRawServices",
473
                replacement="getRawServices")
474
    @security.protected(permissions.View)
475
    def getRawService(self):
476
        return self.getRawServices()
477
478
    @security.protected(permissions.View)
479
    def getServices(self):
480
        """Returns a list of service objects
481
482
        >>> self.getServices()
483
        [<AnalysisService at ...>,  <AnalysisService at ...>, ...]
484
485
        :returns: List of analysis service objects
486
        """
487
        services_uids = self.getRawServices()
488
        return list(map(api.get_object, services_uids))
489
490 View Code Duplication
    @security.protected(permissions.ModifyPortalContent)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
491
    def setServices(self, value):
492
        """Set services for the template
493
494
        This method accepts either a list of analysis service objects, a list
495
        of analysis service UIDs or a list of analysis profile service records
496
        containing the key `uid`:
497
498
        >>> self.setServices([<AnalysisService at ...>, ...])
499
        >>> self.setServices(['353e1d9bd45d45dbabc837114a9c41e6', '...', ...])
500
        >>> self.setServices([{'uid': '...'}, ...])
501
502
        Raises a TypeError if the value does not match any allowed type.
503
        """
504
        if not isinstance(value, list):
505
            value = [value]
506
507
        records = []
508
        for v in value:
509
            uid = ""
510
            if isinstance(v, dict):
511
                uid = api.get_uid(v.get("uid"))
512
            elif api.is_object(v):
513
                uid = api.get_uid(v)
514
            elif api.is_uid(v):
515
                uid = v
516
            else:
517
                raise TypeError(
518
                    "Expected object, uid or record, got %r" % type(v))
519
            records.append({
520
                "uid": uid,
521
            })
522
523
        mutator = self.mutator("services")
524
        mutator(self, records)
525
526
    # BBB: AT schema field property
527
    Services = property(getServices, setServices)
528
529
    @security.private
530
    def get_instrument_query(self):
531
        """Return the preferred instruments
532
        """
533
        query = {
534
            "portal_type": "Instrument",
535
            "is_active": True,
536
            "sort_on": "sortable_title",
537
            "sort_order": "ascending",
538
        }
539
540
        # Restrict available instruments to those with the selected method
541
        method = self.getRestrictToMethod()
542 View Code Duplication
        if method:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
543
            # prepare subquery
544
            method_uid = method.UID()
545
            uids = []
546
            brains = api.search(query, SETUP_CATALOG)
547
            for brain in brains:
548
                uid = api.get_uid(brain)
549
                instrument = api.get_object(brain)
550
                if method_uid in instrument.getRawMethods():
551
                    uids.append(uid)
552
            # create a simple UID query
553
            query = {"UID": uids}
554
        return query
555