Total Complexity | 78 |
Total Lines | 739 |
Duplicated Lines | 1.62 % |
Changes | 0 |
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:
Complex classes like bika.lims.content.instrument 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 | # Copyright 2018 by it's authors. |
||
6 | # Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. |
||
7 | |||
8 | from datetime import date |
||
9 | |||
10 | from AccessControl import ClassSecurityInfo |
||
11 | |||
12 | from Products.CMFCore.utils import getToolByName |
||
13 | from Products.CMFPlone.utils import safe_unicode |
||
14 | from Products.Archetypes.atapi import DisplayList, PicklistWidget |
||
15 | from Products.Archetypes.atapi import registerType |
||
16 | from bika.lims.api.analysis import is_out_of_range |
||
17 | from bika.lims.catalog.analysis_catalog import CATALOG_ANALYSIS_LISTING |
||
18 | from zope.component._api import getAdapters |
||
19 | |||
20 | from zope.interface import implements |
||
21 | from plone.app.folder.folder import ATFolder |
||
22 | |||
23 | # Schema and Fields |
||
24 | from Products.Archetypes.atapi import Schema |
||
25 | from Products.ATContentTypes.content import schemata |
||
26 | from Products.Archetypes.atapi import ReferenceField |
||
27 | from Products.Archetypes.atapi import ComputedField |
||
28 | from Products.Archetypes.atapi import DateTimeField |
||
29 | from Products.Archetypes.atapi import StringField |
||
30 | from Products.Archetypes.atapi import TextField |
||
31 | from Products.Archetypes.atapi import ImageField |
||
32 | from Products.Archetypes.atapi import BooleanField |
||
33 | from Products.ATExtensions.ateapi import RecordsField |
||
34 | from plone.app.blob.field import FileField as BlobFileField |
||
35 | from bika.lims.browser.fields import UIDReferenceField |
||
36 | |||
37 | # Widgets |
||
38 | from Products.Archetypes.atapi import ComputedWidget |
||
39 | from Products.Archetypes.atapi import StringWidget |
||
40 | from Products.Archetypes.atapi import TextAreaWidget |
||
41 | from Products.Archetypes.atapi import FileWidget |
||
42 | from Products.Archetypes.atapi import ImageWidget |
||
43 | from Products.Archetypes.atapi import BooleanWidget |
||
44 | from Products.Archetypes.atapi import SelectionWidget |
||
45 | from Products.Archetypes.atapi import ReferenceWidget |
||
46 | from Products.Archetypes.atapi import MultiSelectionWidget |
||
47 | from bika.lims.browser.widgets import DateTimeWidget |
||
48 | from bika.lims.browser.widgets import RecordsWidget |
||
49 | |||
50 | # bika.lims imports |
||
51 | from bika.lims import api |
||
52 | from bika.lims import logger |
||
53 | from bika.lims.utils import t |
||
54 | from bika.lims.utils import to_utf8 |
||
55 | from bika.lims.config import PROJECTNAME |
||
56 | from bika.lims.exportimport import instruments |
||
57 | from bika.lims.interfaces import IInstrument |
||
58 | from bika.lims.config import QCANALYSIS_TYPES |
||
59 | from bika.lims.content.bikaschema import BikaSchema |
||
60 | from bika.lims.content.bikaschema import BikaFolderSchema |
||
61 | from bika.lims import bikaMessageFactory as _ |
||
62 | |||
63 | schema = BikaFolderSchema.copy() + BikaSchema.copy() + Schema(( |
||
64 | |||
65 | ReferenceField( |
||
66 | 'InstrumentType', |
||
67 | vocabulary='getInstrumentTypes', |
||
68 | allowed_types=('InstrumentType',), |
||
69 | relationship='InstrumentInstrumentType', |
||
70 | required=1, |
||
71 | widget=SelectionWidget( |
||
72 | format='select', |
||
73 | label=_("Instrument type"), |
||
74 | visible={'view': 'invisible', 'edit': 'visible'} |
||
75 | ), |
||
76 | ), |
||
77 | |||
78 | ReferenceField( |
||
79 | 'Manufacturer', |
||
80 | vocabulary='getManufacturers', |
||
81 | allowed_types=('Manufacturer',), |
||
82 | relationship='InstrumentManufacturer', |
||
83 | required=1, |
||
84 | widget=SelectionWidget( |
||
85 | format='select', |
||
86 | label=_("Manufacturer"), |
||
87 | visible={'view': 'invisible', 'edit': 'visible'} |
||
88 | ), |
||
89 | ), |
||
90 | |||
91 | ReferenceField( |
||
92 | 'Supplier', |
||
93 | vocabulary='getSuppliers', |
||
94 | allowed_types=('Supplier',), |
||
95 | relationship='InstrumentSupplier', |
||
96 | required=1, |
||
97 | widget=SelectionWidget( |
||
98 | format='select', |
||
99 | label=_("Supplier"), |
||
100 | visible={'view': 'invisible', 'edit': 'visible'} |
||
101 | ), |
||
102 | ), |
||
103 | |||
104 | StringField( |
||
105 | 'Model', |
||
106 | widget=StringWidget( |
||
107 | label=_("Model"), |
||
108 | description=_("The instrument's model number"), |
||
109 | ) |
||
110 | ), |
||
111 | |||
112 | StringField( |
||
113 | 'SerialNo', |
||
114 | widget=StringWidget( |
||
115 | label=_("Serial No"), |
||
116 | description=_("The serial number that uniquely identifies the instrument"), |
||
117 | ) |
||
118 | ), |
||
119 | |||
120 | UIDReferenceField( |
||
121 | 'Method', |
||
122 | vocabulary='_getAvailableMethods', |
||
123 | allowed_types=('Method',), |
||
124 | required=0, |
||
125 | widget=SelectionWidget( |
||
126 | format='select', |
||
127 | label=_("Method"), |
||
128 | visible=False, |
||
129 | ), |
||
130 | ), |
||
131 | |||
132 | ReferenceField( |
||
133 | 'Methods', |
||
134 | vocabulary='_getAvailableMethods', |
||
135 | allowed_types=('Method',), |
||
136 | relationship='InstrumentMethods', |
||
137 | required=0, |
||
138 | multiValued=1, |
||
139 | widget=PicklistWidget( |
||
140 | size=10, |
||
141 | label=_("Methods"), |
||
142 | ), |
||
143 | ), |
||
144 | |||
145 | BooleanField( |
||
146 | 'DisposeUntilNextCalibrationTest', |
||
147 | default=False, |
||
148 | widget=BooleanWidget( |
||
149 | label=_("De-activate until next calibration test"), |
||
150 | description=_("If checked, the instrument will be unavailable until the next valid " |
||
151 | "calibration was performed. This checkbox will automatically be unchecked."), |
||
152 | ), |
||
153 | ), |
||
154 | |||
155 | # Procedures |
||
156 | TextField( |
||
157 | 'InlabCalibrationProcedure', |
||
158 | schemata='Procedures', |
||
159 | default_content_type='text/plain', |
||
160 | allowed_content_types=('text/plain', ), |
||
161 | default_output_type="text/plain", |
||
162 | widget=TextAreaWidget( |
||
163 | label=_("In-lab calibration procedure"), |
||
164 | description=_("Instructions for in-lab regular calibration routines intended for analysts"), |
||
165 | ), |
||
166 | ), |
||
167 | |||
168 | TextField( |
||
169 | 'PreventiveMaintenanceProcedure', |
||
170 | schemata='Procedures', |
||
171 | default_content_type='text/plain', |
||
172 | allowed_content_types=('text/plain', ), |
||
173 | default_output_type="text/plain", |
||
174 | widget=TextAreaWidget( |
||
175 | label=_("Preventive maintenance procedure"), |
||
176 | description=_("Instructions for regular preventive and maintenance routines intended for analysts"), |
||
177 | ), |
||
178 | ), |
||
179 | |||
180 | StringField( |
||
181 | 'DataInterface', |
||
182 | vocabulary="getExportDataInterfacesList", |
||
183 | widget=SelectionWidget( |
||
184 | checkbox_bound=0, |
||
185 | label=_("Data Interface"), |
||
186 | description=_("Select an Export interface for this instrument."), |
||
187 | format='select', |
||
188 | default='', |
||
189 | visible=True, |
||
190 | ), |
||
191 | ), |
||
192 | |||
193 | StringField('ImportDataInterface', |
||
194 | vocabulary="getImportDataInterfacesList", |
||
195 | multiValued=1, |
||
196 | widget=MultiSelectionWidget( |
||
197 | checkbox_bound=0, |
||
198 | label=_("Import Data Interface"), |
||
199 | description=_( |
||
200 | "Select an Import interface for this instrument."), |
||
201 | format='select', |
||
202 | default='', |
||
203 | visible=True, |
||
204 | ), |
||
205 | ), |
||
206 | |||
207 | RecordsField( |
||
208 | 'ResultFilesFolder', |
||
209 | subfields=('InterfaceName', 'Folder'), |
||
210 | subfield_labels={'InterfaceName': _('Interface Code'), |
||
211 | 'Folder': _('Folder that results will be saved')}, |
||
212 | subfield_readonly={'InterfaceName': True, |
||
213 | 'Folder': False}, |
||
214 | widget=RecordsWidget( |
||
215 | label=_("Result files folders"), |
||
216 | description=_("For each interface of this instrument, \ |
||
217 | you can define a folder where \ |
||
218 | the system should look for the results files while \ |
||
219 | automatically importing results. Having a folder \ |
||
220 | for each Instrument and inside that folder creating \ |
||
221 | different folders for each of its Interfaces \ |
||
222 | can be a good approach. You can use Interface codes \ |
||
223 | to be sure that folder names are unique."), |
||
224 | visible=True, |
||
225 | ), |
||
226 | ), |
||
227 | |||
228 | RecordsField( |
||
229 | 'DataInterfaceOptions', |
||
230 | type='interfaceoptions', |
||
231 | subfields=('Key', 'Value'), |
||
232 | required_subfields=('Key', 'Value'), |
||
233 | subfield_labels={ |
||
234 | 'OptionValue': _('Key'), |
||
235 | 'OptionText': _('Value'), |
||
236 | }, |
||
237 | widget=RecordsWidget( |
||
238 | label=_("Data Interface Options"), |
||
239 | description=_("Use this field to pass arbitrary parameters to the export/import modules."), |
||
240 | visible=False, |
||
241 | ), |
||
242 | ), |
||
243 | |||
244 | ComputedField( |
||
245 | 'Valid', |
||
246 | expression="'1' if context.isValid() else '0'", |
||
247 | widget=ComputedWidget( |
||
248 | visible=False, |
||
249 | ), |
||
250 | ), |
||
251 | |||
252 | # Needed since InstrumentType is sorted by its own object, not by its name. |
||
253 | ComputedField( |
||
254 | 'InstrumentTypeName', |
||
255 | expression='here.getInstrumentType().Title() if here.getInstrumentType() else ""', |
||
256 | widget=ComputedWidget( |
||
257 | label=_('Instrument Type'), |
||
258 | visible=True, |
||
259 | ), |
||
260 | ), |
||
261 | |||
262 | ComputedField( |
||
263 | 'InstrumentLocationName', |
||
264 | expression='here.getInstrumentLocation().Title() if here.getInstrumentLocation() else ""', |
||
265 | widget=ComputedWidget( |
||
266 | label=_("Instrument Location"), |
||
267 | label_msgid="instrument_location", |
||
268 | description=_("The room and location where the instrument is installed"), |
||
269 | description_msgid="help_instrument_location", |
||
270 | visible=True, |
||
271 | ), |
||
272 | ), |
||
273 | |||
274 | ComputedField( |
||
275 | 'ManufacturerName', |
||
276 | expression='here.getManufacturer().Title() if here.getManufacturer() else ""', |
||
277 | widget=ComputedWidget( |
||
278 | label=_('Manufacturer'), |
||
279 | visible=True, |
||
280 | ), |
||
281 | ), |
||
282 | |||
283 | ComputedField( |
||
284 | 'SupplierName', |
||
285 | expression='here.getSupplier().Title() if here.getSupplier() else ""', |
||
286 | widget=ComputedWidget( |
||
287 | label=_('Supplier'), |
||
288 | visible=True, |
||
289 | ), |
||
290 | ), |
||
291 | |||
292 | StringField( |
||
293 | 'AssetNumber', |
||
294 | widget=StringWidget( |
||
295 | label=_("Asset Number"), |
||
296 | description=_("The instrument's ID in the lab's asset register"), |
||
297 | ) |
||
298 | ), |
||
299 | |||
300 | ReferenceField( |
||
301 | 'InstrumentLocation', |
||
302 | schemata='Additional info.', |
||
303 | vocabulary='getInstrumentLocations', |
||
304 | allowed_types=('InstrumentLocation', ), |
||
305 | relationship='InstrumentInstrumentLocation', |
||
306 | required=0, |
||
307 | widget=SelectionWidget( |
||
308 | format='select', |
||
309 | label=_("Instrument Location"), |
||
310 | label_msgid="instrument_location", |
||
311 | description=_("The room and location where the instrument is installed"), |
||
312 | description_msgid="help_instrument_location", |
||
313 | visible={'view': 'invisible', 'edit': 'visible'} |
||
314 | ) |
||
315 | ), |
||
316 | |||
317 | ImageField( |
||
318 | 'Photo', |
||
319 | schemata='Additional info.', |
||
320 | widget=ImageWidget( |
||
321 | label=_("Photo image file"), |
||
322 | description=_("Photo of the instrument"), |
||
323 | ), |
||
324 | ), |
||
325 | |||
326 | DateTimeField( |
||
327 | 'InstallationDate', |
||
328 | schemata='Additional info.', |
||
329 | widget=DateTimeWidget( |
||
330 | label=_("InstallationDate"), |
||
331 | description=_("The date the instrument was installed"), |
||
332 | ) |
||
333 | ), |
||
334 | |||
335 | BlobFileField( |
||
336 | 'InstallationCertificate', |
||
337 | schemata='Additional info.', |
||
338 | widget=FileWidget( |
||
339 | label=_("Installation Certificate"), |
||
340 | description=_("Installation certificate upload"), |
||
341 | ) |
||
342 | ), |
||
343 | |||
344 | )) |
||
345 | |||
346 | schema.moveField('AssetNumber', before='description') |
||
347 | schema.moveField('SupplierName', before='Model') |
||
348 | schema.moveField('ManufacturerName', before='SupplierName') |
||
349 | schema.moveField('InstrumentTypeName', before='ManufacturerName') |
||
350 | |||
351 | schema['description'].widget.visible = True |
||
352 | schema['description'].schemata = 'default' |
||
353 | |||
354 | def getMaintenanceTypes(context): |
||
355 | types = [('preventive', 'Preventive'), |
||
356 | ('repair', 'Repair'), |
||
357 | ('enhance', 'Enhancement')] |
||
358 | return DisplayList(types) |
||
359 | |||
360 | |||
361 | def getCalibrationAgents(context): |
||
362 | agents = [('analyst', 'Analyst'), |
||
363 | ('maintainer', 'Maintainer')] |
||
364 | return DisplayList(agents) |
||
365 | |||
366 | |||
367 | class Instrument(ATFolder): |
||
368 | """A physical gadget of the lab |
||
369 | """ |
||
370 | implements(IInstrument) |
||
371 | security = ClassSecurityInfo() |
||
372 | displayContentsTab = False |
||
373 | schema = schema |
||
|
|||
374 | |||
375 | _at_rename_after_creation = True |
||
376 | |||
377 | def _renameAfterCreation(self, check_auto_id=False): |
||
378 | from bika.lims.idserver import renameAfterCreation |
||
379 | renameAfterCreation(self) |
||
380 | |||
381 | def Title(self): |
||
382 | return to_utf8(safe_unicode(self.title)) |
||
383 | |||
384 | def getDataInterfacesList(self, type_interface="import"): |
||
385 | interfaces = list() |
||
386 | if type_interface == "export": |
||
387 | interfaces = instruments.get_instrument_export_interfaces() |
||
388 | elif type_interface == "import": |
||
389 | interfaces = instruments.get_instrument_import_interfaces() |
||
390 | interfaces = map(lambda imp: (imp[0], imp[1].title), interfaces) |
||
391 | interfaces.sort(lambda x, y: cmp(x[1].lower(), y[1].lower())) |
||
392 | interfaces.insert(0, ('', t(_('None')))) |
||
393 | return DisplayList(interfaces) |
||
394 | |||
395 | def getExportDataInterfacesList(self): |
||
396 | return self.getDataInterfacesList("export") |
||
397 | |||
398 | def getImportDataInterfacesList(self): |
||
399 | return self.getDataInterfacesList("import") |
||
400 | |||
401 | def getScheduleTaskTypesList(self): |
||
402 | return getMaintenanceTypes(self) |
||
403 | |||
404 | def getMaintenanceTypesList(self): |
||
405 | return getMaintenanceTypes(self) |
||
406 | |||
407 | def getCalibrationAgentsList(self): |
||
408 | return getCalibrationAgents(self) |
||
409 | |||
410 | def getManufacturers(self): |
||
411 | bsc = getToolByName(self, 'bika_setup_catalog') |
||
412 | items = [(c.UID, c.Title) |
||
413 | for c in bsc(portal_type='Manufacturer', |
||
414 | inactive_state='active')] |
||
415 | items.sort(lambda x, y: cmp(x[1], y[1])) |
||
416 | return DisplayList(items) |
||
417 | |||
418 | def getMethodUIDs(self): |
||
419 | uids = [] |
||
420 | if self.getMethods(): |
||
421 | uids = [m.UID() for m in self.getMethods()] |
||
422 | return uids |
||
423 | |||
424 | def getSuppliers(self): |
||
425 | bsc = getToolByName(self, 'bika_setup_catalog') |
||
426 | items = [(c.UID, c.getName) |
||
427 | for c in bsc(portal_type='Supplier', |
||
428 | inactive_state='active')] |
||
429 | items.sort(lambda x, y: cmp(x[1], y[1])) |
||
430 | return DisplayList(items) |
||
431 | |||
432 | View Code Duplication | def _getAvailableMethods(self): |
|
433 | """ Returns the available (active) methods. |
||
434 | One method can be done by multiple instruments, but one |
||
435 | instrument can only be used in one method. |
||
436 | """ |
||
437 | bsc = getToolByName(self, 'bika_setup_catalog') |
||
438 | items = [(c.UID, c.Title) |
||
439 | for c in bsc(portal_type='Method', |
||
440 | inactive_state='active')] |
||
441 | items.sort(lambda x, y: cmp(x[1], y[1])) |
||
442 | items.insert(0, ('', t(_('None')))) |
||
443 | return DisplayList(items) |
||
444 | |||
445 | def getInstrumentTypes(self): |
||
446 | bsc = getToolByName(self, 'bika_setup_catalog') |
||
447 | items = [(c.UID, c.Title) |
||
448 | for c in bsc(portal_type='InstrumentType', |
||
449 | inactive_state='active')] |
||
450 | items.sort(lambda x, y: cmp(x[1], y[1])) |
||
451 | return DisplayList(items) |
||
452 | |||
453 | def getInstrumentLocations(self): |
||
454 | bsc = getToolByName(self, 'bika_setup_catalog') |
||
455 | items = [(c.UID, c.Title) |
||
456 | for c in bsc(portal_type='InstrumentLocation', |
||
457 | inactive_state='active')] |
||
458 | items.sort(lambda x, y: cmp(x[1], y[1])) |
||
459 | items.insert(0, ('', t(_('None')))) |
||
460 | return DisplayList(items) |
||
461 | |||
462 | def getMaintenanceTasks(self): |
||
463 | return self.objectValues('InstrumentMaintenanceTask') |
||
464 | |||
465 | def getCalibrations(self): |
||
466 | """ Return all calibration objects related with the instrument |
||
467 | """ |
||
468 | return self.objectValues('InstrumentCalibration') |
||
469 | |||
470 | def getCertifications(self): |
||
471 | """ Returns the certifications of the instrument. Both internal |
||
472 | and external certifitions |
||
473 | """ |
||
474 | return self.objectValues('InstrumentCertification') |
||
475 | |||
476 | def getValidCertifications(self): |
||
477 | """ Returns the certifications fully valid |
||
478 | """ |
||
479 | certs = [] |
||
480 | today = date.today() |
||
481 | for c in self.getCertifications(): |
||
482 | validfrom = c.getValidFrom() if c else None |
||
483 | validto = c.getValidTo() if validfrom else None |
||
484 | if not validfrom or not validto: |
||
485 | continue |
||
486 | validfrom = validfrom.asdatetime().date() |
||
487 | validto = validto.asdatetime().date() |
||
488 | if (today >= validfrom and today <= validto): |
||
489 | certs.append(c) |
||
490 | return certs |
||
491 | |||
492 | def isValid(self): |
||
493 | """ Returns if the current instrument is not out for verification, calibration, |
||
494 | out-of-date regards to its certificates and if the latest QC succeed |
||
495 | """ |
||
496 | return self.isOutOfDate() is False \ |
||
497 | and self.isQCValid() is True \ |
||
498 | and self.getDisposeUntilNextCalibrationTest() is False \ |
||
499 | and self.isValidationInProgress() is False \ |
||
500 | and self.isCalibrationInProgress() is False |
||
501 | |||
502 | def isQCValid(self): |
||
503 | """ Returns True if the results of the last batch of QC Analyses |
||
504 | performed against this instrument was within the valid range. |
||
505 | |||
506 | For a given Reference Sample, more than one Reference Analyses assigned |
||
507 | to this same instrument can be performed and the Results Capture Date |
||
508 | might slightly differ amongst them. Thus, this function gets the latest |
||
509 | QC Analysis performed, looks for siblings (through RefAnalysisGroupID) |
||
510 | and if the results for all them are valid, then returns True. If there |
||
511 | is one single Reference Analysis from the group with an out-of-range |
||
512 | result, the function returns False |
||
513 | """ |
||
514 | query = {"portal_type": "ReferenceAnalysis", |
||
515 | "getInstrumentUID": self.UID(), |
||
516 | "sort_on": "getResultCaptureDate", |
||
517 | "sort_order": "reverse", |
||
518 | "sort_limit": 1,} |
||
519 | brains = api.search(query, CATALOG_ANALYSIS_LISTING) |
||
520 | if len(brains) == 0: |
||
521 | # There are no Reference Analyses assigned to this instrument yet |
||
522 | return True |
||
523 | |||
524 | # Look for siblings. These are the QC Analyses that were created |
||
525 | # together with this last ReferenceAnalysis and for the same Reference |
||
526 | # Sample. If they were added through "Add Reference Analyses" in a |
||
527 | # Worksheet, they typically appear in the same slot. |
||
528 | group_id = brains[0].getReferenceAnalysesGroupID |
||
529 | query = {"portal_type": "ReferenceAnalysis", |
||
530 | "getInstrumentUID": self.UID(), |
||
531 | "getReferenceAnalysesGroupID": group_id,} |
||
532 | brains = api.search(query, CATALOG_ANALYSIS_LISTING) |
||
533 | for brain in brains: |
||
534 | results_range = brain.getResultsRange |
||
535 | if not results_range: |
||
536 | continue |
||
537 | # Is out of range? |
||
538 | out_of_range = is_out_of_range(brain)[0] |
||
539 | if out_of_range: |
||
540 | return False |
||
541 | |||
542 | # By default, in range |
||
543 | return True |
||
544 | |||
545 | def isOutOfDate(self): |
||
546 | """ Returns if the current instrument is out-of-date regards to |
||
547 | its certifications |
||
548 | """ |
||
549 | certification = self.getLatestValidCertification() |
||
550 | if certification: |
||
551 | return not certification.isValid() |
||
552 | return True |
||
553 | |||
554 | def isValidationInProgress(self): |
||
555 | """ Returns if the current instrument is under validation progress |
||
556 | """ |
||
557 | validation = self.getLatestValidValidation() |
||
558 | if validation: |
||
559 | return validation.isValidationInProgress() |
||
560 | return False |
||
561 | |||
562 | def isCalibrationInProgress(self): |
||
563 | """ Returns if the current instrument is under calibration progress |
||
564 | """ |
||
565 | calibration = self.getLatestValidCalibration() |
||
566 | if calibration is not None: |
||
567 | return calibration.isCalibrationInProgress() |
||
568 | return False |
||
569 | |||
570 | def getCertificateExpireDate(self): |
||
571 | """ Returns the current instrument's data expiration certificate |
||
572 | """ |
||
573 | certification = self.getLatestValidCertification() |
||
574 | if certification: |
||
575 | return certification.getValidTo() |
||
576 | return None |
||
577 | |||
578 | def getWeeksToExpire(self): |
||
579 | """ Returns the amount of weeks and days untils it's certification expire |
||
580 | """ |
||
581 | certification = self.getLatestValidCertification() |
||
582 | if certification: |
||
583 | return certification.getWeeksAndDaysToExpire() |
||
584 | return 0, 0 |
||
585 | |||
586 | def getLatestValidCertification(self): |
||
587 | """Returns the certification with the most remaining days until expiration. |
||
588 | If no certification was found, it returns None. |
||
589 | """ |
||
590 | |||
591 | # 1. get all certifications |
||
592 | certifications = self.getCertifications() |
||
593 | |||
594 | # 2. filter out certifications, which are invalid |
||
595 | valid_certifications = filter(lambda x: x.isValid(), certifications) |
||
596 | |||
597 | # 3. sort by the remaining days to expire, e.g. [10, 7, 6, 1] |
||
598 | def sort_func(x, y): |
||
599 | return cmp(x.getDaysToExpire(), y.getDaysToExpire()) |
||
600 | sorted_certifications = sorted(valid_certifications, cmp=sort_func, reverse=True) |
||
601 | |||
602 | # 4. return the certification with the most remaining days |
||
603 | if len(sorted_certifications) > 0: |
||
604 | return sorted_certifications[0] |
||
605 | return None |
||
606 | |||
607 | def getLatestValidValidation(self): |
||
608 | """Returns the validation with the most remaining days in validation. |
||
609 | If no validation was found, it returns None. |
||
610 | """ |
||
611 | # 1. get all validations |
||
612 | validations = self.getValidations() |
||
613 | |||
614 | # 2. filter out validations, which are not in progress |
||
615 | active_validations = filter(lambda x: x.isValidationInProgress(), validations) |
||
616 | |||
617 | # 3. sort by the remaining days in validation, e.g. [10, 7, 6, 1] |
||
618 | def sort_func(x, y): |
||
619 | return cmp(x.getRemainingDaysInValidation(), y.getRemainingDaysInValidation()) |
||
620 | sorted_validations = sorted(active_validations, cmp=sort_func, reverse=True) |
||
621 | |||
622 | # 4. return the validation with the most remaining days |
||
623 | if len(sorted_validations) > 0: |
||
624 | return sorted_validations[0] |
||
625 | return None |
||
626 | |||
627 | def getLatestValidCalibration(self): |
||
628 | """Returns the calibration with the most remaining days in calibration. |
||
629 | If no calibration was found, it returns None. |
||
630 | """ |
||
631 | # 1. get all calibrations |
||
632 | calibrations = self.getCalibrations() |
||
633 | |||
634 | # 2. filter out calibrations, which are not in progress |
||
635 | active_calibrations = filter(lambda x: x.isCalibrationInProgress(), calibrations) |
||
636 | |||
637 | # 3. sort by the remaining days in calibration, e.g. [10, 7, 6, 1] |
||
638 | def sort_func(x, y): |
||
639 | return cmp(x.getRemainingDaysInCalibration(), y.getRemainingDaysInCalibration()) |
||
640 | sorted_calibrations = sorted(active_calibrations, cmp=sort_func, reverse=True) |
||
641 | |||
642 | # 4. return the calibration with the most remaining days |
||
643 | if len(sorted_calibrations) > 0: |
||
644 | return sorted_calibrations[0] |
||
645 | return None |
||
646 | |||
647 | def getValidations(self): |
||
648 | """ Return all the validations objects related with the instrument |
||
649 | """ |
||
650 | return self.objectValues('InstrumentValidation') |
||
651 | |||
652 | def getDocuments(self): |
||
653 | """ Return all the multifile objects related with the instrument |
||
654 | """ |
||
655 | return self.objectValues('Multifile') |
||
656 | |||
657 | def getSchedule(self): |
||
658 | return self.objectValues('InstrumentScheduledTask') |
||
659 | |||
660 | def addReferences(self, reference, service_uids): |
||
661 | """ Add reference analyses to reference |
||
662 | """ |
||
663 | # TODO Workflow - Analyses. Assignment of refanalysis to Instrument |
||
664 | addedanalyses = [] |
||
665 | wf = getToolByName(self, 'portal_workflow') |
||
666 | bsc = getToolByName(self, 'bika_setup_catalog') |
||
667 | bac = getToolByName(self, 'bika_analysis_catalog') |
||
668 | ref_type = reference.getBlank() and 'b' or 'c' |
||
669 | ref_uid = reference.UID() |
||
670 | postfix = 1 |
||
671 | for refa in reference.getReferenceAnalyses(): |
||
672 | grid = refa.getReferenceAnalysesGroupID() |
||
673 | try: |
||
674 | cand = int(grid.split('-')[2]) |
||
675 | if cand >= postfix: |
||
676 | postfix = cand + 1 |
||
677 | except: |
||
678 | pass |
||
679 | postfix = str(postfix).zfill(int(3)) |
||
680 | refgid = 'I%s-%s' % (reference.id, postfix) |
||
681 | for service_uid in service_uids: |
||
682 | # services with dependents don't belong in references |
||
683 | service = bsc(portal_type='AnalysisService', UID=service_uid)[0].getObject() |
||
684 | calc = service.getCalculation() |
||
685 | if calc and calc.getDependentServices(): |
||
686 | continue |
||
687 | ref_analysis = reference.addReferenceAnalysis(service) |
||
688 | |||
689 | # Set ReferenceAnalysesGroupID (same id for the analyses from |
||
690 | # the same Reference Sample and same Worksheet) |
||
691 | # https://github.com/bikalabs/Bika-LIMS/issues/931 |
||
692 | ref_analysis.setReferenceAnalysesGroupID(refgid) |
||
693 | ref_analysis.setInstrument(self) |
||
694 | ref_analysis.reindexObject() |
||
695 | addedanalyses.append(ref_analysis) |
||
696 | |||
697 | # Set DisposeUntilNextCalibrationTest to False |
||
698 | if (len(addedanalyses) > 0): |
||
699 | self.getField('DisposeUntilNextCalibrationTest').set(self, False) |
||
700 | |||
701 | return addedanalyses |
||
702 | |||
703 | def setImportDataInterface(self, values): |
||
704 | """ Return the current list of import data interfaces |
||
705 | """ |
||
706 | exims = self.getImportDataInterfacesList() |
||
707 | new_values = [value for value in values if value in exims] |
||
708 | if len(new_values) < len(values): |
||
709 | logger.warn("Some Interfaces weren't added...") |
||
710 | self.Schema().getField('ImportDataInterface').set(self, new_values) |
||
711 | |||
712 | def displayValue(self, vocab, value, widget): |
||
713 | """Overwrite the Script (Python) `displayValue.py` located at |
||
714 | `Products.Archetypes.skins.archetypes` to handle the references |
||
715 | of our Picklist Widget (Methods) gracefully. |
||
716 | This method gets called by the `picklist.pt` template like this: |
||
717 | |||
718 | display python:context.displayValue(vocab, value, widget);" |
||
719 | """ |
||
720 | # Taken from the Script (Python) |
||
721 | t = self.restrictedTraverse('@@at_utils').translate |
||
722 | |||
723 | # ensure we have strings, otherwise the `getValue` method of |
||
724 | # Products.Archetypes.utils will raise a TypeError |
||
725 | def to_string(v): |
||
726 | if isinstance(v, basestring): |
||
727 | return v |
||
728 | return api.get_title(v) |
||
729 | |||
730 | if isinstance(value, (list, tuple)): |
||
731 | value = map(to_string, value) |
||
732 | |||
733 | return t(vocab, value, widget) |
||
734 | |||
735 | |||
736 | schemata.finalizeATCTSchema(schema, folderish=True, moveDiscussion=False) |
||
737 | |||
738 | registerType(Instrument, PROJECTNAME) |
||
739 |