Passed
Push — master ( 70be41...e1228c )
by Ramon
11:12
created

AnalysisService._getAvailableInstrumentsDisplayList()   A

Complexity

Conditions 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 12
Ratio 100 %

Importance

Changes 0
Metric Value
eloc 8
dl 12
loc 12
rs 10
c 0
b 0
f 0
cc 2
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-2019 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from AccessControl import ClassSecurityInfo
22
from bika.lims import PMF
23
from bika.lims import api
24
from bika.lims import bikaMessageFactory as _
25
from bika.lims.browser.fields import InterimFieldsField
26
from bika.lims.browser.fields import UIDReferenceField
27
from bika.lims.browser.widgets.partitionsetupwidget import PartitionSetupWidget
28
from bika.lims.browser.widgets.recordswidget import RecordsWidget
29
from bika.lims.browser.widgets.referencewidget import ReferenceWidget
30
from bika.lims.config import PROJECTNAME
31
from bika.lims.content.abstractbaseanalysis import AbstractBaseAnalysis
32
from bika.lims.content.abstractbaseanalysis import schema
33
from bika.lims.interfaces import IAnalysisService
34
from bika.lims.interfaces import IDeactivable
35
from bika.lims.utils import to_utf8 as _c
36
from magnitude import mg
37
from Products.Archetypes.public import BooleanField
38
from Products.Archetypes.public import BooleanWidget
39
from Products.Archetypes.public import DisplayList
40
from Products.Archetypes.public import MultiSelectionWidget
41
from Products.Archetypes.public import Schema
42
from Products.Archetypes.public import SelectionWidget
43
from Products.Archetypes.public import registerType
44
from Products.Archetypes.Registry import registerField
45
from Products.ATExtensions.ateapi import RecordsField
46
from Products.CMFCore.utils import getToolByName
47
from zope.interface import implements
48
49
50
def getContainers(instance,
51
                  minvol=None,
52
                  allow_blank=True,
53
                  show_container_types=True,
54
                  show_containers=True):
55
    """ Containers vocabulary
56
57
    This is a separate class so that it can be called from ajax to filter
58
    the container list, as well as being used as the AT field vocabulary.
59
60
    Returns a tuple of tuples: ((object_uid, object_title), ())
61
62
    If the partition is flagged 'Separate', only containers are displayed.
63
    If the Separate flag is false, displays container types.
64
65
    XXX bsc = self.portal.bika_setup_catalog
66
    XXX obj = bsc(getKeyword='Moist')[0].getObject()
67
    XXX u'Container Type: Canvas bag' in obj.getContainers().values()
68
    XXX True
69
70
    """
71
72
    bsc = getToolByName(instance, 'bika_setup_catalog')
73
74
    items = [['', _('Any')]] if allow_blank else []
75
76
    containers = []
77
    for container in bsc(portal_type='Container', sort_on='sortable_title'):
78
        container = container.getObject()
79
80
        # verify container capacity is large enough for required sample volume.
81
        if minvol is not None:
82
            capacity = container.getCapacity()
83
            try:
84
                capacity = capacity.split(' ', 1)
85
                capacity = mg(float(capacity[0]), capacity[1])
86
                if capacity < minvol:
87
                    continue
88
            except (ValueError, TypeError):
89
                # if there's a unit conversion error, allow the container
90
                # to be displayed.
91
                pass
92
93
        containers.append(container)
94
95
    if show_containers:
96
        # containers with no containertype first
97
        for container in containers:
98
            if not container.getContainerType():
99
                items.append([container.UID(), container.Title()])
100
101
    ts = getToolByName(instance, 'translation_service').translate
102
    cat_str = _c(ts(_('Container Type')))
103
    containertypes = [c.getContainerType() for c in containers]
104
    containertypes = dict([(ct.UID(), ct.Title())
105
                           for ct in containertypes if ct])
106
    for ctype_uid, ctype_title in containertypes.items():
107
        ctype_title = _c(ctype_title)
108
        if show_container_types:
109
            items.append([ctype_uid, "%s: %s" % (cat_str, ctype_title)])
110
        if show_containers:
111
            for container in containers:
112
                ctype = container.getContainerType()
113
                if ctype and ctype.UID() == ctype_uid:
114
                    items.append([container.UID(), container.Title()])
115
116
    items = tuple(items)
117
    return items
118
119
120
class PartitionSetupField(RecordsField):
121
    _properties = RecordsField._properties.copy()
122
    _properties.update({
123
        'subfields': (
124
            'sampletype',
125
            'separate',
126
            'preservation',
127
            'container',
128
            'vol',
129
            # 'retentionperiod',
130
        ),
131
        'subfield_labels': {
132
            'sampletype': _('Sample Type'),
133
            'separate': _('Separate Container'),
134
            'preservation': _('Preservation'),
135
            'container': _('Container'),
136
            'vol': _('Required Volume'),
137
            # 'retentionperiod': _('Retention Period'),
138
        },
139
        'subfield_types': {
140
            'separate': 'boolean',
141
            'vol': 'string',
142
            'container': 'selection',
143
            'preservation': 'selection',
144
        },
145
        'subfield_vocabularies': {
146
            'sampletype': 'SampleTypes',
147
            'preservation': 'Preservations',
148
            'container': 'Containers',
149
        },
150
        'subfield_sizes': {
151
            'sampletype': 1,
152
            'preservation': 6,
153
            'vol': 8,
154
            'container': 6,
155
            # 'retentionperiod':10,
156
        }
157
    })
158
    security = ClassSecurityInfo()
159
160
    security.declarePublic('SampleTypes')
161
162
    def SampleTypes(self, instance=None):
163
        instance = instance or self
164
        bsc = getToolByName(instance, 'bika_setup_catalog')
165
        items = []
166
        for st in bsc(portal_type='SampleType',
167
                      is_active=True,
168
                      sort_on='sortable_title'):
169
            st = st.getObject()
170
            title = st.Title()
171
            items.append((st.UID(), title))
172
        items = [['', '']] + list(items)
173
        return DisplayList(items)
174
175
    security.declarePublic('Preservations')
176
177
    def Preservations(self, instance=None):
178
        instance = instance or self
179
        bsc = getToolByName(instance, 'bika_setup_catalog')
180
        items = [[c.UID, c.title] for c in
181
                 bsc(portal_type='Preservation',
182
                     is_active=True,
183
                     sort_on='sortable_title')]
184
        items = [['', _('Any')]] + list(items)
185
        return DisplayList(items)
186
187
    security.declarePublic('Containers')
188
189
    def Containers(self, instance=None):
190
        instance = instance or self
191
        items = getContainers(instance, allow_blank=True)
192
        return DisplayList(items)
193
194
195
registerField(PartitionSetupField, title="", description="")
196
197
198
# If this flag is true, then analyses created from this service will be linked
199
# to their own Sample Partition, and no other analyses will be linked to that
200
# partition.
201
Separate = BooleanField(
202
    'Separate',
203
    schemata='Container and Preservation',
204
    default=False,
205
    required=0,
206
    widget=BooleanWidget(
207
        label=_("Separate Container"),
208
        description=_("Check this box to ensure a separate sample container is "
209
                      "used for this analysis service"),
210
    )
211
)
212
213
# The preservation for this service; If multiple services share the same
214
# preservation, then it's possible that they can be performed on the same
215
# sample partition.
216
Preservation = UIDReferenceField(
217
    'Preservation',
218
    schemata='Container and Preservation',
219
    allowed_types=('Preservation',),
220
    vocabulary='getPreservations',
221
    required=0,
222
    multiValued=0,
223
    widget=ReferenceWidget(
224
        checkbox_bound=0,
225
        label=_("Default Preservation"),
226
        description=_(
227
            "Select a default preservation for this analysis service. If the "
228
            "preservation depends on the sample type combination, specify a "
229
            "preservation per sample type in the table below"),
230
        catalog_name='bika_setup_catalog',
231
        base_query={'is_active': True},
232
    )
233
)
234
235
# The container or containertype for this service's analyses can be specified.
236
# If multiple services share the same container or containertype, then it's
237
# possible that their analyses can be performed on the same partitions
238
Container = UIDReferenceField(
239
    'Container',
240
    schemata='Container and Preservation',
241
    allowed_types=('Container', 'ContainerType'),
242
    vocabulary='getContainers',
243
    required=0,
244
    multiValued=0,
245
    widget=ReferenceWidget(
246
        checkbox_bound=0,
247
        label=_("Default Container"),
248
        description=_(
249
            "Select the default container to be used for this analysis "
250
            "service. If the container to be used depends on the sample type "
251
            "and preservation combination, specify the container in the "
252
            "sample type table below"),
253
        catalog_name='bika_setup_catalog',
254
        base_query={'is_active': True},
255
    )
256
)
257
258
# This is a list of dictionaries which contains the PartitionSetupWidget
259
# settings.  This is used to decide how many distinct physical partitions
260
# will be created, which containers/preservations they will use, and which
261
# analyases can be performed on each partition.
262
PartitionSetup = PartitionSetupField(
263
    'PartitionSetup',
264
    schemata='Container and Preservation',
265
    widget=PartitionSetupWidget(
266
        label=PMF("Preservation per sample type"),
267
        description=_(
268
            "Please specify preservations that differ from the analysis "
269
            "service's default preservation per sample type here."),
270
    )
271
)
272
273
# Allow/Disallow to set the calculation manually
274
# Behavior controlled by javascript depending on Instruments field:
275
# - If no instruments available, hide and uncheck
276
# - If at least one instrument selected then checked, but not readonly
277
# See browser/js/bika.lims.analysisservice.edit.js
278
UseDefaultCalculation = BooleanField(
279
    'UseDefaultCalculation',
280
    schemata="Method",
281
    default=True,
282
    widget=BooleanWidget(
283
        label=_("Use the Default Calculation of Method"),
284
        description=_(
285
            "Select if the calculation to be used is the calculation set by "
286
            "default in the default method. If unselected, the calculation "
287
            "can be selected manually"),
288
    )
289
)
290
291
# Manual methods associated to the AS
292
# List of methods capable to perform the Analysis Service. The
293
# Methods selected here are displayed in the Analysis Request
294
# Add view, closer to this Analysis Service if selected.
295
# Use getAvailableMethods() to retrieve the list with methods both
296
# from selected instruments and manually entered.
297
# Behavior controlled by js depending on ManualEntry/Instrument:
298
# - If InstrumentEntry not checked, show
299
# See browser/js/bika.lims.analysisservice.edit.js
300
Methods = UIDReferenceField(
301
    'Methods',
302
    schemata="Method",
303
    required=0,
304
    multiValued=1,
305
    vocabulary='_getAvailableMethodsDisplayList',
306
    allowed_types=('Method',),
307
    accessor="getMethodUIDs",
308
    widget=MultiSelectionWidget(
309
        label=_("Methods"),
310
        description=_(
311
            "The tests of this type of analysis can be performed by using "
312
            "more than one method with the 'Manual entry of results' option "
313
            "enabled. A selection list with the methods selected here is "
314
            "populated in the manage results view for each test of this type "
315
            "of analysis. Note that only methods with 'Allow manual entry' "
316
            "option enabled are displayed here; if you want the user to be "
317
            "able to assign a method that requires instrument entry, enable "
318
            "the 'Instrument assignment is allowed' option."),
319
    )
320
)
321
322
# Instruments associated to the AS
323
# List of instruments capable to perform the Analysis Service. The
324
# Instruments selected here are displayed in the Analysis Request
325
# Add view, closer to this Analysis Service if selected.
326
# - If InstrumentEntry not checked, hide and unset
327
# - If InstrumentEntry checked, set the first selected and show
328
Instruments = UIDReferenceField(
329
    'Instruments',
330
    schemata="Method",
331
    required=0,
332
    multiValued=1,
333
    vocabulary='_getAvailableInstrumentsDisplayList',
334
    allowed_types=('Instrument',),
335
    accessor="getInstrumentUIDs",
336
    widget=MultiSelectionWidget(
337
        label=_("Instruments"),
338
        description=_(
339
            "More than one instrument can be used in a test of this type of "
340
            "analysis. A selection list with the instruments selected here is "
341
            "populated in the results manage view for each test of this type "
342
            "of analysis. The available instruments in the selection list "
343
            "will change in accordance with the method selected by the user "
344
            "for that test in the manage results view. Although a method can "
345
            "have more than one instrument assigned, the selection list is "
346
            "only populated with the instruments that are both set here and "
347
            "allowed for the selected method."),
348
    )
349
)
350
351
# Calculation to be used. This field is used in Analysis Service Edit view.
352
#
353
# AnalysisService defines a setter to maintain back-references on the
354
# calculation, so that calculations can easily lookup their dependants based
355
# on this field's value.
356
#
357
#  The default calculation is the one linked to the default method Behavior
358
# controlled by js depending on UseDefaultCalculation:
359
# - If UseDefaultCalculation is set to False, show this field
360
# - If UseDefaultCalculation is set to True, show this field
361
#  See browser/js/bika.lims.analysisservice.edit.js
362
Calculation = UIDReferenceField(
363
    "Calculation",
364
    schemata="Method",
365
    required=0,
366
    vocabulary="_getAvailableCalculationsDisplayList",
367
    allowed_types=("Calculation",),
368
    accessor="getCalculationUID",
369
    widget=SelectionWidget(
370
        format="select",
371
        label=_("Calculation"),
372
        description=_("Calculation to be assigned to this content."),
373
        catalog_name="bika_setup_catalog",
374
        base_query={"is_active": True},
375
    )
376
)
377
378
# InterimFields are defined in Calculations, Services, and Analyses.
379
# In Analysis Services, the default values are taken from Calculation.
380
# In Analyses, the default values are taken from the Analysis Service.
381
# When instrument results are imported, the values in analysis are overridden
382
# before the calculation is performed.
383
InterimFields = InterimFieldsField(
384
    'InterimFields',
385
    schemata='Method',
386
    widget=RecordsWidget(
387
        label=_("Additional Values"),
388
        description=_(
389
            "Values can be entered here which will override the defaults "
390
            "specified in the Calculation Interim Fields."),
391
    )
392
)
393
394
schema = schema.copy() + Schema((
395
    Separate,
396
    Preservation,
397
    Container,
398
    PartitionSetup,
399
    Methods,
400
    Instruments,
401
    UseDefaultCalculation,
402
    Calculation,
403
    InterimFields,
404
))
405
406
# Re-order some fields from AbstractBaseAnalysis schema.
407
# Adding them to the Schema(()) above does not work.
408
schema.moveField('ManualEntryOfResults', after='PartitionSetup')
409
schema.moveField('Methods', after='ManualEntryOfResults')
410
schema.moveField('InstrumentEntryOfResults', after='Methods')
411
schema.moveField('Instruments', after='InstrumentEntryOfResults')
412
schema.moveField('Instrument', after='Instruments')
413
schema.moveField('Method', after='Instrument')
414
schema.moveField('Calculation', after='UseDefaultCalculation')
415
schema.moveField('DuplicateVariation', after='Calculation')
416
schema.moveField('Accredited', after='Calculation')
417
schema.moveField('InterimFields', after='Calculation')
418
419
420
class AnalysisService(AbstractBaseAnalysis):
421
    implements(IAnalysisService, IDeactivable)
422
    security = ClassSecurityInfo()
423
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
424
    displayContentsTab = False
425
    _at_rename_after_creation = True
426
427
    def _renameAfterCreation(self, check_auto_id=False):
428
        from bika.lims.idserver import renameAfterCreation
429
430
        return renameAfterCreation(self)
431
432
    @security.public
433
    def getCalculation(self):
434
        """Returns the assigned calculation
435
436
        :returns: Calculation object
437
        """
438
        return self.getField("Calculation").get(self)
439
440
    @security.public
441
    def getCalculationUID(self):
442
        """Returns the UID of the assigned calculation
443
444
        NOTE: This is the default accessor of the `Calculation` schema field
445
        and needed for the selection widget to render the selected value
446
        properly in _view_ mode.
447
448
        :returns: Calculation UID
449
        """
450
        calculation = self.getCalculation()
451
        if not calculation:
452
            return None
453
        return api.get_uid(calculation)
454
455
    @security.public
456
    def getContainers(self, instance=None):
457
        # On first render, the containers must be filtered
458
        instance = instance or self
459
        separate = self.getSeparate()
460
        containers = getContainers(instance,
461
                                   allow_blank=True,
462
                                   show_container_types=not separate,
463
                                   show_containers=separate)
464
        return DisplayList(containers)
465
466
    def getPreservations(self):
467
        bsc = getToolByName(self, 'bika_setup_catalog')
468
        items = [(o.UID, o.Title) for o in
469
                 bsc(portal_type='Preservation', is_active=True)]
470
        items.sort(lambda x, y: cmp(x[1], y[1]))
471
        return DisplayList(list(items))
472
473
    @security.public
474
    def getAvailableMethods(self):
475
        """ Returns the methods available for this analysis.
476
            If the service has the getInstrumentEntryOfResults(), returns
477
            the methods available from the instruments capable to perform
478
            the service, as well as the methods set manually for the
479
            analysis on its edit view. If getInstrumentEntryOfResults()
480
            is unset, only the methods assigned manually to that service
481
            are returned.
482
        """
483
        if not self.getInstrumentEntryOfResults():
484
            # No need to go further, just return the manually assigned methods
485
            return self.getMethods()
486
487
        # Return the manually assigned methods plus those from instruments
488
        method_uids = self.getAvailableMethodUIDs()
489
        query = dict(portal_type="Method", UID=method_uids)
490
        brains = api.search(query, "bika_setup_catalog")
491
        return map(api.get_object_by_uid, brains)
492
493
    @security.public
494
    def getAvailableMethodUIDs(self):
495
        """
496
        Returns the UIDs of the available methods. it is used as a
497
        vocabulary to fill the selection list of 'Methods' field.
498
        """
499
        method_uids = self.getRawMethods()
500
        if self.getInstrumentEntryOfResults():
501
            for instrument in self.getInstruments():
502
                method_uids.extend(instrument.getRawMethods())
503
            method_uids = filter(None, method_uids)
504
            method_uids = list(set(method_uids))
505
        return method_uids
506
507
    @security.public
508
    def getMethods(self):
509
        """Returns the assigned methods
510
511
        If you want to obtain the available methods to assign to the service,
512
        use getAvailableMethodUIDs.
513
514
        :returns: List of method objects
515
        """
516
        return self.getField("Methods").get(self)
517
518
    @security.public
519
    def getMethodUIDs(self):
520
        """Returns the UIDs of the assigned methods
521
522
        NOTE: This is the default accessor of the `Methods` schema field
523
        and needed for the multiselection widget to render the selected values
524
        properly in _view_ mode.
525
526
        :returns: List of method UIDs
527
        """
528
        return self.getRawMethods()
529
530
    @security.public
531
    def getInstruments(self):
532
        """Returns the assigned instruments
533
534
        :returns: List of instrument objects
535
        """
536
        return self.getField("Instruments").get(self)
537
538
    @security.public
539
    def getInstrumentUIDs(self):
540
        """Returns the UIDs of the assigned instruments
541
542
        NOTE: This is the default accessor of the `Instruments` schema field
543
        and needed for the multiselection widget to render the selected values
544
        properly in _view_ mode.
545
546
        :returns: List of instrument UIDs
547
        """
548
        return self.getRawInstruments()
549
550
    @security.public
551
    def getAvailableInstruments(self):
552
        """ Returns the instruments available for this service.
553
            If the service has the getInstrumentEntryOfResults(), returns
554
            the instruments capable to perform this service. Otherwhise,
555
            returns an empty list.
556
        """
557
        instruments = self.getInstruments() \
558
            if self.getInstrumentEntryOfResults() is True \
559
            else None
560
        return instruments if instruments else []
561
562
    @security.private
563
    def _getAvailableMethodsDisplayList(self):
564
        """ Returns a DisplayList with the available Methods
565
            registered in Bika-Setup. Only active Methods and those
566
            with Manual Entry field active are fetched.
567
            Used to fill the Methods MultiSelectionWidget when 'Allow
568
            Instrument Entry of Results is not selected'.
569
        """
570
        bsc = getToolByName(self, 'bika_setup_catalog')
571
        items = [(i.UID, i.Title)
572
                 for i in bsc(portal_type='Method',
573
                              is_active=True)
574
                 if i.getObject().isManualEntryOfResults()]
575
        items.sort(lambda x, y: cmp(x[1], y[1]))
576
        items.insert(0, ('', _("None")))
577
        return DisplayList(list(items))
578
579
    @security.private
580
    def _getAvailableCalculationsDisplayList(self):
581
        """ Returns a DisplayList with the available Calculations
582
            registered in Bika-Setup. Only active Calculations are
583
            fetched. Used to fill the Calculation field
584
        """
585
        bsc = getToolByName(self, 'bika_setup_catalog')
586
        items = [(i.UID, i.Title)
587
                 for i in bsc(portal_type='Calculation',
588
                              is_active=True)]
589
        items.sort(lambda x, y: cmp(x[1], y[1]))
590
        items.insert(0, ('', _("None")))
591
        return DisplayList(list(items))
592
593 View Code Duplication
    @security.private
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
594
    def _getAvailableInstrumentsDisplayList(self):
595
        """ Returns a DisplayList with the available Instruments
596
            registered in Bika-Setup. Only active Instruments are
597
            fetched. Used to fill the Instruments MultiSelectionWidget
598
        """
599
        bsc = getToolByName(self, 'bika_setup_catalog')
600
        items = [(i.UID, i.Title)
601
                 for i in bsc(portal_type='Instrument',
602
                              is_active=True)]
603
        items.sort(lambda x, y: cmp(x[1], y[1]))
604
        return DisplayList(list(items))
605
606
    @security.public
607
    def getServiceDependencies(self):
608
        """
609
        This methods returns a list with the analyses services dependencies.
610
        :return: a list of analysis services objects.
611
        """
612
        calc = self.getCalculation()
613
        if calc:
614
            return calc.getCalculationDependencies(flat=True)
615
        return []
616
617
    @security.public
618
    def getServiceDependenciesUIDs(self):
619
        """
620
        This methods returns a list with the service dependencies UIDs
621
        :return: a list of uids
622
        """
623
        deps = self.getServiceDependencies()
624
        deps_uids = [service.UID() for service in deps]
625
        return deps_uids
626
627
    @security.public
628
    def getServiceDependants(self):
629
        bsc = getToolByName(self, 'bika_setup_catalog')
630
        active_calcs = bsc(portal_type='Calculation', is_active=True)
631
        calculations = [c.getObject() for c in active_calcs]
632
        dependants = []
633
        for calc in calculations:
634
            calc_dependants = calc.getDependentServices()
635
            if self in calc_dependants:
636
                calc_dependencies = calc.getCalculationDependants()
637
                dependants = dependants + calc_dependencies
638
        dependants = list(set(dependants))
639
        if self in dependants:
640
            dependants.remove(self)
641
        return dependants
642
643
    @security.public
644
    def getServiceDependantsUIDs(self):
645
        deps = self.getServiceDependants()
646
        deps_uids = [service.UID() for service in deps]
647
        return deps_uids
648
649
    @security.public
650
    def after_deactivate_transition_event(self):
651
        """Method triggered after a 'deactivate' transition for the current
652
        AnalysisService is performed. Removes this service from the Analysis
653
        Profiles or Analysis Request Templates where is assigned.
654
        This function is called automatically by
655
        bika.lims.workflow.AfterTransitionEventHandler
656
        """
657
        # Remove the service from profiles to which is assigned
658
        profiles = self.getBackReferences('AnalysisProfileAnalysisService')
659
        for profile in profiles:
660
            profile.remove_service(self)
661
662
        # Remove the service from templates to which is assigned
663
        bsc = api.get_tool('bika_setup_catalog')
664
        templates = bsc(portal_type='ARTemplate')
665
        for template in templates:
666
            template = api.get_object(template)
667
            template.remove_service(self)
668
669
670
registerType(AnalysisService, PROJECTNAME)
671