Skip to content

Commit 7c71549

Browse files
authored
Merge pull request #729 from Lunga001/add_instrument_varian_vistapro
Add instrument varian vistapro
2 parents 8d6f7c8 + 62e6f67 commit 7c71549

File tree

11 files changed

+927
-4
lines changed

11 files changed

+927
-4
lines changed

CHANGES.rst

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Changelog
66

77
**Added**
88

9+
- #729 Added Instrument: Varian Vista-PRO ICP
910
- #694 Added "Warn Min" and "Warn Max" subfields in Analysis Specifications
1011
- #710 Added more builtin functions for Calculations
1112

bika/lims/exportimport/instruments/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from myself import myinstrument
4141
from nuclisens import easyq
4242
from genexpert import genexpert
43+
from varian.vistapro import icp
4344

4445
__all__ = ['abaxis.vetscan.vs2',
4546
'abbott.m2000rt.m2000rt',
@@ -79,6 +80,7 @@
7980
'thermoscientific.arena.xt20',
8081
'thermoscientific.gallery.Ts9861x',
8182
'thermoscientific.multiskan.go',
83+
'varian.vistapro.icp',
8284
]
8385

8486
# Parsers are for auto-import. If empty, then auto-import won't wun for that
@@ -122,6 +124,7 @@
122124
['myself.myinstrument', 'MyInstrumentCSVParser'],
123125
['nuclisens.easyq', 'EasyQXMLParser'],
124126
['genexpert.genexpert', 'GeneXpertParser'],
127+
['varian.vistapro.icp', 'VistaPROICPParser'],
125128
]
126129

127130

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
9+
def get_instrument_import_search_criteria(sample):
10+
sam = ['getRequestID', 'getSampleID', 'getClientSampleID']
11+
if sample == 'requestid':
12+
sam = ['getRequestID']
13+
if sample == 'sampleid':
14+
sam = ['getSampleID']
15+
elif sample == 'clientsid':
16+
sam = ['getClientSampleID']
17+
elif sample == 'sample_clientsid':
18+
sam = ['getSampleID', 'getClientSampleID']
19+
return sam
20+
21+
22+
def get_instrument_import_override(override):
23+
over = [False, False]
24+
if override == 'nooverride':
25+
over = [False, False]
26+
elif override == 'override':
27+
over = [True, False]
28+
elif override == 'overrideempty':
29+
over = [True, True]
30+
return over
31+
32+
33+
def get_instrument_import_ar_allowed_states(artoapply):
34+
status = ['sample_received', 'attachment_due', 'to_be_verified']
35+
if artoapply == 'received':
36+
status = ['sample_received']
37+
elif artoapply == 'received_tobeverified':
38+
status = ['sample_received', 'attachment_due', 'to_be_verified']
39+
return status
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# -*- coding: utf-8 -*-

bika/lims/exportimport/instruments/varian/vistapro/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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+
""" Varian Vista-PRO ICP
9+
"""
10+
import logging
11+
import re
12+
13+
from DateTime import DateTime
14+
from Products.CMFCore.utils import getToolByName
15+
from plone.i18n.normalizer.interfaces import IIDNormalizer
16+
from zope.component import getUtility
17+
18+
from bika.lims import api
19+
from bika.lims.browser import BrowserView
20+
from bika.lims.exportimport.instruments.utils import \
21+
(get_instrument_import_search_criteria,
22+
get_instrument_import_override,
23+
get_instrument_import_ar_allowed_states)
24+
from bika.lims.exportimport.instruments.resultsimport import \
25+
InstrumentCSVResultsFileParser, AnalysisResultsImporter
26+
from bika.lims import bikaMessageFactory as _
27+
import json
28+
import traceback
29+
30+
logger = logging.getLogger(__name__)
31+
32+
title = "Varian Vista-PRO ICP"
33+
34+
35+
class VistaPROICPParser(InstrumentCSVResultsFileParser):
36+
""" Vista-PRO Parser
37+
"""
38+
def __init__(self, csv):
39+
InstrumentCSVResultsFileParser.__init__(self, csv)
40+
self._end_header = False
41+
self._resultsheader = []
42+
self._numline = 0
43+
44+
def _parseline(self, line):
45+
if self._end_header:
46+
return self.parse_resultsline(line)
47+
return self.parse_headerline(line)
48+
49+
def parse_headerline(self, line):
50+
""" Parses header lines
51+
"""
52+
if self._end_header is True:
53+
# Header already processed
54+
return 0
55+
56+
splitted = [token.strip() for token in line.split(',')]
57+
if splitted[0] == 'Solution Label':
58+
self._resultsheader = splitted
59+
self._end_header = True
60+
return 0
61+
62+
def parse_resultsline(self, line):
63+
""" CSV Parser
64+
"""
65+
66+
splitted = [token.strip() for token in line.split(',')]
67+
if self._end_header:
68+
resid = splitted[0]
69+
rawdict = {'DefaultResult': 'Soln Conc'}
70+
rawdict.update(dict(zip(self._resultsheader, splitted)))
71+
date_string = "{Date} {Time}".format(**rawdict)
72+
date_time = DateTime(date_string)
73+
rawdict['DateTime'] = date_time
74+
element = rawdict.get("Element", "").replace(" ", "").replace(".", "")
75+
76+
# Set DefaultResult to 0.0 if result is "0" or "--" or '' or 'ND'
77+
result = rawdict[rawdict['DefaultResult']]
78+
column_name = rawdict['DefaultResult']
79+
result = self.get_result(column_name, result, line)
80+
rawdict[rawdict['DefaultResult']] = result
81+
#
82+
83+
val = re.sub(r"\W", "", element)
84+
self._addRawResult(resid, values={val: rawdict}, override=False)
85+
86+
self.log(
87+
"End of file reached successfully: ${total_objects} objects, "
88+
"${total_analyses} analyses, ${total_results} results",
89+
mapping={"total_objects": self.getObjectsTotalCount(),
90+
"total_analyses": self.getAnalysesTotalCount(),
91+
"total_results": self.getResultsTotalCount()}
92+
)
93+
94+
def get_result(self, column_name, result, line):
95+
result = str(result)
96+
if result.startswith('--') or result == '' or result == 'ND':
97+
return 0.0
98+
99+
if api.is_floatable(result):
100+
result = api.to_float(result)
101+
return result > 0.0 and result or 0.0
102+
self.err("No valid number ${result} in column (${column_name})",
103+
mapping={"result": result,
104+
"column_name": column_name},
105+
numline=self._numline, line=line)
106+
return
107+
108+
109+
110+
class VistaPROICPImporter(AnalysisResultsImporter):
111+
""" Importer
112+
"""
113+
114+
def __init__(self, parser, context, idsearchcriteria, override,
115+
allowed_ar_states=None, allowed_analysis_states=None,
116+
instrument_uid=None):
117+
118+
AnalysisResultsImporter.__init__(self,
119+
parser,
120+
context,
121+
idsearchcriteria,
122+
override,
123+
allowed_ar_states,
124+
allowed_analysis_states,
125+
instrument_uid)
126+
127+
128+
def Import(context, request):
129+
""" Import Form
130+
"""
131+
form = request.form
132+
# form['file'] sometimes returns a list
133+
infile = form['instrument_results_file'][0] if \
134+
isinstance(form['instrument_results_file'], list) \
135+
else form['instrument_results_file']
136+
override = form['results_override']
137+
artoapply = form['artoapply']
138+
sample = form.get('sample', 'requestid')
139+
instrument = form.get('instrument', None)
140+
errors = []
141+
logs = []
142+
warns = []
143+
144+
# Load the most suitable parser according to file extension/options/etc...
145+
parser = None
146+
if not hasattr(infile, 'filename'):
147+
errors.append(_("No file selected"))
148+
149+
parser = VistaPROICPParser(infile)
150+
status = get_instrument_import_ar_allowed_states(artoapply)
151+
over = get_instrument_import_override(override)
152+
sam = get_instrument_import_search_criteria(sample)
153+
154+
importer = VistaPROICPImporter(parser=parser,
155+
context=context,
156+
idsearchcriteria=sam,
157+
allowed_ar_states=status,
158+
allowed_analysis_states=None,
159+
override=over,
160+
instrument_uid=instrument)
161+
tbex = ''
162+
try:
163+
importer.process()
164+
except:
165+
tbex = traceback.format_exc()
166+
errors = importer.errors
167+
logs = importer.logs
168+
warns = importer.warns
169+
if tbex:
170+
errors.append(tbex)
171+
172+
results = {'errors': errors, 'log': logs, 'warns': warns}
173+
174+
return json.dumps(results)
175+
176+
177+
class Export(BrowserView):
178+
""" Writes worksheet analyses to a CSV file for Varian Vista Pro ICP.
179+
Sends the CSV file to the response for download by the browser.
180+
"""
181+
182+
def __init__(self, context, request):
183+
self.context = context
184+
self.request = request
185+
186+
def __call__(self, analyses):
187+
uc = getToolByName(self.context, 'uid_catalog')
188+
instrument = self.context.getInstrument()
189+
norm = getUtility(IIDNormalizer).normalize
190+
# We use ".sin" extension, but really it's just a little CSV inside.
191+
filename = '{}-{}.sin'.format(self.context.getId(),
192+
norm(instrument.getDataInterface()))
193+
194+
# write rows, one per Sample, including including refs and duplicates.
195+
# COL A: "sample-id*sampletype-title" (yes that's a '*').
196+
# COL B: " ",
197+
# COL C: " ",
198+
# COL D: " ",
199+
# COL E: 1.000000000,
200+
# COL F: 1.000000,
201+
# COL G: 1.0000000
202+
# If routine analysis, COL B is the AR ID + sample type.
203+
# If Reference analysis, COL B is the Ref Sample.
204+
# If Duplicate analysis, COL B is the Worksheet.
205+
lyt = self.context.getLayout()
206+
lyt.sort(cmp=lambda x, y: cmp(int(x['position']), int(y['position'])))
207+
rows = []
208+
209+
# These are always the same on each row
210+
b = '" "'
211+
c = '" "'
212+
d = '" "'
213+
e = '1.000000000'
214+
f = '1.000000'
215+
g = '1.0000000'
216+
217+
result = ''
218+
# We don't want to include every single slot! Just one entry
219+
# per AR, Duplicate, or Control.
220+
used_ids = []
221+
for x, row in enumerate(lyt):
222+
a_uid = row['analysis_uid']
223+
c_uid = row['container_uid']
224+
analysis = uc(UID=a_uid)[0].getObject() if a_uid else None
225+
container = uc(UID=c_uid)[0].getObject() if c_uid else None
226+
if row['type'] == 'a':
227+
if 'a{}'.format(container.id) in used_ids:
228+
continue
229+
used_ids.append('a{}'.format(container.id))
230+
sample = container.getSample()
231+
samplepoint = sample.getSamplePoint()
232+
sp_title = samplepoint.Title() if samplepoint else ''
233+
a = '"{}*{}"'.format(container.id, sp_title)
234+
elif row['type'] in 'bcd':
235+
refgid = analysis.getReferenceAnalysesGroupID()
236+
if 'bcd{}'.format(refgid) in used_ids:
237+
continue
238+
used_ids.append('bcd{}'.format(refgid))
239+
a = refgid
240+
rows.append(','.join([a, b, c, d, e, f, g]))
241+
result += '\r\n'.join(rows)
242+
243+
# stream to browser
244+
setheader = self.request.RESPONSE.setHeader
245+
setheader('Content-Length', len(result))
246+
setheader('Content-Type', 'text/comma-separated-values')
247+
setheader('Content-Disposition', 'inline; filename=%s' % filename)
248+
self.request.RESPONSE.write(result)

0 commit comments

Comments
 (0)