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