Passed
Push — master ( b60410...062e8c )
by Jordi
05:05
created

ARImport.validate_headers()   F

Complexity

Conditions 20

Size

Total Lines 65
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 46
dl 0
loc 65
rs 0
c 0
b 0
f 0
cc 20
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like bika.lims.content.arimport.ARImport.validate_headers() 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 AccessControl import ClassSecurityInfo
22
import csv
23
from copy import deepcopy
24
from DateTime.DateTime import DateTime
25
from Products.Archetypes.event import ObjectInitializedEvent
26
from Products.CMFCore.WorkflowCore import WorkflowException
27
from bika.lims import bikaMessageFactory as _
28
from bika.lims.browser import ulocalized_time
29
from bika.lims.config import PROJECTNAME
30
from bika.lims.content.bikaschema import BikaSchema
31
from bika.lims.content.analysisrequest import schema as ar_schema
32
from bika.lims.content.sample import schema as sample_schema
33
from bika.lims.idserver import renameAfterCreation
34
from bika.lims.interfaces import IARImport, IClient
35
from bika.lims.utils import tmpID
36
from bika.lims.utils.analysisrequest import create_analysisrequest
37
from bika.lims.vocabularies import CatalogVocabulary
38
from bika.lims.workflow import doActionFor
39
from collective.progressbar.events import InitialiseProgressBar
40
from collective.progressbar.events import ProgressBar
41
from collective.progressbar.events import ProgressState
42
from collective.progressbar.events import UpdateProgressEvent
43
from Products.Archetypes import atapi
44
from Products.Archetypes.public import *
45
from plone.app.blob.field import FileField as BlobFileField
46
from Products.Archetypes.references import HoldingReference
47
from Products.Archetypes.utils import addStatusMessage
48
from Products.CMFCore.utils import getToolByName
49
from Products.CMFPlone.utils import _createObjectByType
50
from Products.DataGridField import CheckboxColumn
51
from Products.DataGridField import Column
52
from Products.DataGridField import DataGridField
53
from Products.DataGridField import DataGridWidget
54
from Products.DataGridField import DateColumn
55
from Products.DataGridField import LinesColumn
56
from Products.DataGridField import SelectColumn
57
from zope import event
58
from zope.event import notify
59
from zope.i18nmessageid import MessageFactory
60
from zope.interface import implements
61
62
from bika.lims.browser.widgets import ReferenceWidget as bReferenceWidget
63
64
import sys
65
import transaction
66
67
_p = MessageFactory(u"plone")
68
69
OriginalFile = BlobFileField(
70
    'OriginalFile',
71
    widget=ComputedWidget(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable ComputedWidget does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'ComputedWidget'
Loading history...
72
        visible=False
73
    ),
74
)
75
76
Filename = StringField(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable StringField does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'StringField'
Loading history...
77
    'Filename',
78
    widget=StringWidget(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable StringWidget does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'StringWidget'
Loading history...
79
        label=_('Original Filename'),
80
        visible=True
81
    ),
82
)
83
84
NrSamples = StringField(
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'StringField'
Loading history...
85
    'NrSamples',
86
    widget=StringWidget(
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'StringWidget'
Loading history...
87
        label=_('Number of samples'),
88
        visible=True
89
    ),
90
)
91
92
ClientName = StringField(
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'StringField'
Loading history...
93
    'ClientName',
94
    searchable=True,
95
    widget=StringWidget(
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'StringWidget'
Loading history...
96
        label=_("Client Name"),
97
    ),
98
)
99
100
ClientID = StringField(
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'StringField'
Loading history...
101
    'ClientID',
102
    searchable=True,
103
    widget=StringWidget(
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'StringWidget'
Loading history...
104
        label=_('Client ID'),
105
    ),
106
)
107
108
ClientOrderNumber = StringField(
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'StringField'
Loading history...
109
    'ClientOrderNumber',
110
    searchable=True,
111
    widget=StringWidget(
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'StringWidget'
Loading history...
112
        label=_('Client Order Number'),
113
    ),
114
)
115
116
ClientReference = StringField(
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'StringField'
Loading history...
117
    'ClientReference',
118
    searchable=True,
119
    widget=StringWidget(
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'StringWidget'
Loading history...
120
        label=_('Client Reference'),
121
    ),
122
)
123
124
Contact = ReferenceField(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable ReferenceField does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'ReferenceField'
Loading history...
125
    'Contact',
126
    allowed_types=('Contact',),
127
    relationship='ARImportContact',
128
    default_method='getContactUIDForUser',
129
    referenceClass=HoldingReference,
130
    vocabulary_display_path_bound=sys.maxint,
131
    widget=ReferenceWidget(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable ReferenceWidget does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'ReferenceWidget'
Loading history...
132
        label=_('Primary Contact'),
133
        size=20,
134
        visible=True,
135
        base_query={'is_active': True},
136
        showOn=True,
137
        popup_width='300px',
138
        colModel=[{'columnName': 'UID', 'hidden': True},
139
                  {'columnName': 'Fullname', 'width': '100',
140
                   'label': _('Name')}],
141
    ),
142
)
143
144
Batch = ReferenceField(
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'ReferenceField'
Loading history...
145
    'Batch',
146
    allowed_types=('Batch',),
147
    relationship='ARImportBatch',
148
    widget=bReferenceWidget(
149
        label=_('Batch'),
150
        visible=True,
151
        catalog_name='bika_catalog',
152
        base_query={'review_state': 'open'},
153
        showOn=True,
154
    ),
155
)
156
157
CCContacts = DataGridField(
158
    'CCContacts',
159
    allow_insert=False,
160
    allow_delete=False,
161
    allow_reorder=False,
162
    allow_empty_rows=False,
163
    columns=('CCNamesReport',
164
             'CCEmailsReport',
165
             'CCNamesInvoice',
166
             'CCEmailsInvoice'),
167
    default=[{'CCNamesReport': [],
168
              'CCEmailsReport': [],
169
              'CCNamesInvoice': [],
170
              'CCEmailsInvoice': []
171
              }],
172
    widget=DataGridWidget(
173
        columns={
174
            'CCNamesReport': LinesColumn('Report CC Contacts'),
175
            'CCEmailsReport': LinesColumn('Report CC Emails'),
176
            'CCNamesInvoice': LinesColumn('Invoice CC Contacts'),
177
            'CCEmailsInvoice': LinesColumn('Invoice CC Emails')
178
        }
179
    )
180
)
181
182
SampleData = DataGridField(
183
    'SampleData',
184
    allow_insert=True,
185
    allow_delete=True,
186
    allow_reorder=False,
187
    allow_empty_rows=False,
188
    allow_oddeven=True,
189
    columns=('ClientSampleID',
190
             'SamplingDate',
191
             'DateSampled',
192
             'SamplePoint',
193
             'SampleMatrix',
194
             'SampleType',  # not a schema field!
195
             'ContainerType',  # not a schema field!
196
             'Analyses',  # not a schema field!
197
             'Profiles'  # not a schema field!
198
             ),
199
    widget=DataGridWidget(
200
        label=_('Samples'),
201
        columns={
202
            'ClientSampleID': Column('Sample ID'),
203
            'SamplingDate': DateColumn('Sampling Date'),
204
            'DateSampled': DateColumn('Date Sampled'),
205
            'SamplePoint': SelectColumn(
206
                'Sample Point', vocabulary='Vocabulary_SamplePoint'),
207
            'SampleMatrix': SelectColumn(
208
                'Sample Matrix', vocabulary='Vocabulary_SampleMatrix'),
209
            'SampleType': SelectColumn(
210
                'Sample Type', vocabulary='Vocabulary_SampleType'),
211
            'ContainerType': SelectColumn(
212
                'Container', vocabulary='Vocabulary_ContainerType'),
213
            'Analyses': LinesColumn('Analyses'),
214
            'Profiles': LinesColumn('Profiles'),
215
        }
216
    )
217
)
218
219
Errors = LinesField(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable LinesField does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'LinesField'
Loading history...
220
    'Errors',
221
    widget=LinesWidget(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable LinesWidget does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'LinesWidget'
Loading history...
222
        label=_('Errors'),
223
        rows=10,
224
    )
225
)
226
227
schema = BikaSchema.copy() + Schema((
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable Schema does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'Schema'
Loading history...
228
    OriginalFile,
229
    Filename,
230
    NrSamples,
231
    ClientName,
232
    ClientID,
233
    ClientOrderNumber,
234
    ClientReference,
235
    Contact,
236
    CCContacts,
237
    Batch,
238
    SampleData,
239
    Errors,
240
))
241
242
schema['title'].validators = ()
243
# Update the validation layer after change the validator in runtime
244
schema['title']._validationLayer()
245
246
247
class ARImport(BaseFolder):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable BaseFolder does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'BaseFolder'
Loading history...
248
    security = ClassSecurityInfo()
249
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
250
    displayContentsTab = False
251
    implements(IARImport)
252
253
    _at_rename_after_creation = True
254
255
    def _renameAfterCreation(self, check_auto_id=False):
256
        renameAfterCreation(self)
257
258
    def guard_validate_transition(self):
259
        """We may only attempt validation if file data has been uploaded.
260
        """
261
        data = self.getOriginalFile()
262
        if data and len(data):
263
            return True
264
265
    # TODO Workflow - ARImport - Remove
266
    def workflow_before_validate(self):
267
        """This function transposes values from the provided file into the
268
        ARImport object's fields, and checks for invalid values.
269
270
        If errors are found:
271
            - Validation transition is aborted.
272
            - Errors are stored on object and displayed to user.
273
274
        """
275
        # Re-set the errors on this ARImport each time validation is attempted.
276
        # When errors are detected they are immediately appended to this field.
277
        self.setErrors([])
278
279
        self.validate_headers()
280
        self.validate_samples()
281
282
        if self.getErrors():
283
            addStatusMessage(self.REQUEST, _p('Validation errors.'), 'error')
284
            transaction.commit()
285
            self.REQUEST.response.write(
286
                '<script>document.location.href="%s/edit"</script>' % (
287
                    self.absolute_url()))
288
        self.REQUEST.response.write(
289
            '<script>document.location.href="%s/view"</script>' % (
290
                self.absolute_url()))
291
292
    def at_post_edit_script(self):
293
        workflow = getToolByName(self, 'portal_workflow')
294
        trans_ids = [t['id'] for t in workflow.getTransitionsFor(self)]
295
        if 'validate' in trans_ids:
296
            workflow.doActionFor(self, 'validate')
297
298
    def workflow_script_import(self):
299
        """Create objects from valid ARImport
300
        """
301
        bsc = getToolByName(self, 'bika_setup_catalog')
302
        client = self.aq_parent
303
304
        title = _('Submitting Sample Import')
305
        description = _('Creating and initialising objects')
306
        bar = ProgressBar(self, self.REQUEST, title, description)
307
        notify(InitialiseProgressBar(bar))
308
309
        profiles = [x.getObject() for x in bsc(portal_type='AnalysisProfile')]
310
311
        gridrows = self.schema['SampleData'].get(self)
312
        row_cnt = 0
313
        for therow in gridrows:
314
            row = deepcopy(therow)
315
            row_cnt += 1
316
317
            # Profiles are titles, profile keys, or UIDS: convert them to UIDs.
318
            newprofiles = []
319
            for title in row['Profiles']:
320
                objects = [x for x in profiles
321
                           if title in (x.getProfileKey(), x.UID(), x.Title())]
322
                for obj in objects:
323
                    newprofiles.append(obj.UID())
324
            row['Profiles'] = newprofiles
325
326
            # Same for analyses
327
            newanalyses = set(self.get_row_services(row) +
328
                              self.get_row_profile_services(row))
329
            # get batch
330
            batch = self.schema['Batch'].get(self)
331
            if batch:
332
                row['Batch'] = batch.UID()
333
            # Add AR fields from schema into this row's data
334
            row['ClientReference'] = self.getClientReference()
335
            row['ClientOrderNumber'] = self.getClientOrderNumber()
336
            contact_uid =\
337
                self.getContact().UID() if self.getContact() else None
338
            row['Contact'] = contact_uid
339
            # Creating analysis request from gathered data
340
            ar = create_analysisrequest(
341
                client,
342
                self.REQUEST,
343
                row,
344
                analyses=list(newanalyses),)
345
346
            # progress marker update
347
            progress_index = float(row_cnt) / len(gridrows) * 100
348
            progress = ProgressState(self.REQUEST, progress_index)
349
            notify(UpdateProgressEvent(progress))
350
351
        # document has been written to, and redirect() fails here
352
        self.REQUEST.response.write(
353
            '<script>document.location.href="%s"</script>' % (
354
                self.absolute_url()))
355
356
    def get_header_values(self):
357
        """Scrape the "Header" values from the original input file
358
        """
359
        lines = self.getOriginalFile().data.splitlines()
360
        reader = csv.reader(lines)
361
        header_fields = header_data = []
362
        for row in reader:
363
            if not any(row):
364
                continue
365
            if row[0].strip().lower() == 'header':
366
                header_fields = [x.strip() for x in row][1:]
367
                continue
368
            if row[0].strip().lower() == 'header data':
369
                header_data = [x.strip() for x in row][1:]
370
                break
371
        if not (header_data or header_fields):
372
            return None
373
        if not (header_data and header_fields):
374
            self.error("File is missing header row or header data")
375
            return None
376
        # inject us out of here
377
        values = dict(zip(header_fields, header_data))
378
        # blank cell from sheet will probably make it in here:
379
        if '' in values:
380
            del (values[''])
381
        return values
382
383
    def save_header_data(self):
384
        """Save values from the file's header row into their schema fields.
385
        """
386
        client = self.aq_parent
387
388
        headers = self.get_header_values()
389
        if not headers:
390
            return False
391
392
        # Plain header fields that can be set into plain schema fields:
393
        for h, f in [
394
            ('File name', 'Filename'),
395
            ('No of Samples', 'NrSamples'),
396
            ('Client name', 'ClientName'),
397
            ('Client ID', 'ClientID'),
398
            ('Client Order Number', 'ClientOrderNumber'),
399
            ('Client Reference', 'ClientReference')
400
        ]:
401
            v = headers.get(h, None)
402
            if v:
403
                field = self.schema[f]
404
                field.set(self, v)
405
            del (headers[h])
406
407
        # Primary Contact
408
        v = headers.get('Contact', None)
409
        contacts = [x for x in client.objectValues('Contact')]
410
        contact = [c for c in contacts if c.Title() == v]
411
        if contact:
412
            self.schema['Contact'].set(self, contact)
413
        else:
414
            self.error("Specified contact '%s' does not exist; using '%s'"%
415
                       (v, contacts[0].Title()))
416
            self.schema['Contact'].set(self, contacts[0])
417
        del (headers['Contact'])
418
419
        # CCContacts
420
        field_value = {
421
            'CCNamesReport': '',
422
            'CCEmailsReport': '',
423
            'CCNamesInvoice': '',
424
            'CCEmailsInvoice': ''
425
        }
426
        for h, f in [
427
            # csv header name      DataGrid Column ID
428
            ('CC Names - Report', 'CCNamesReport'),
429
            ('CC Emails - Report', 'CCEmailsReport'),
430
            ('CC Names - Invoice', 'CCNamesInvoice'),
431
            ('CC Emails - Invoice', 'CCEmailsInvoice'),
432
        ]:
433
            if h in headers:
434
                values = [x.strip() for x in headers.get(h, '').split(",")]
435
                field_value[f] = values if values else ''
436
                del (headers[h])
437
        self.schema['CCContacts'].set(self, [field_value])
438
439
        if headers:
440
            unexpected = ','.join(headers.keys())
441
            self.error("Unexpected header fields: %s" % unexpected)
442
443
    def get_sample_values(self):
444
        """Read the rows specifying Samples and return a dictionary with
445
        related data.
446
447
        keys are:
448
            headers - row with "Samples" in column 0.  These headers are
449
               used as dictionary keys in the rows below.
450
            prices - Row with "Analysis Price" in column 0.
451
            total_analyses - Row with "Total analyses" in colmn 0
452
            price_totals - Row with "Total price excl Tax" in column 0
453
            samples - All other sample rows.
454
455
        """
456
        res = {'samples': []}
457
        lines = self.getOriginalFile().data.splitlines()
458
        reader = csv.reader(lines)
459
        next_rows_are_sample_rows = False
460
        for row in reader:
461
            if not any(row):
462
                continue
463
            if next_rows_are_sample_rows:
464
                vals = [x.strip() for x in row]
465
                if not any(vals):
466
                    continue
467
                res['samples'].append(zip(res['headers'], vals))
468
            elif row[0].strip().lower() == 'samples':
469
                res['headers'] = [x.strip() for x in row]
470
            elif row[0].strip().lower() == 'analysis price':
471
                res['prices'] = \
472
                    zip(res['headers'], [x.strip() for x in row])
473
            elif row[0].strip().lower() == 'total analyses':
474
                res['total_analyses'] = \
475
                    zip(res['headers'], [x.strip() for x in row])
476
            elif row[0].strip().lower() == 'total price excl tax':
477
                res['price_totals'] = \
478
                    zip(res['headers'], [x.strip() for x in row])
479
                next_rows_are_sample_rows = True
480
        return res
481
482
    def save_sample_data(self):
483
        """Save values from the file's header row into the DataGrid columns
484
        after doing some very basic validation
485
        """
486
        bsc = getToolByName(self, 'bika_setup_catalog')
487
        keywords = self.bika_setup_catalog.uniqueValuesFor('getKeyword')
488
        profiles = []
489
        for p in bsc(portal_type='AnalysisProfile'):
490
            p = p.getObject()
491
            profiles.append(p.Title())
492
            profiles.append(p.getProfileKey())
493
494
        sample_data = self.get_sample_values()
495
        if not sample_data:
496
            return False
497
498
        # columns that we expect, but do not find, are listed here.
499
        # we report on them only once, after looping through sample rows.
500
        missing = set()
501
502
        # This contains all sample header rows that were not handled
503
        # by this code
504
        unexpected = set()
505
506
        # Save other errors here instead of sticking them directly into
507
        # the field, so that they show up after MISSING and before EXPECTED
508
        errors = []
509
510
        # This will be the new sample-data field value, when we are done.
511
        grid_rows = []
512
513
        row_nr = 0
514
        for row in sample_data['samples']:
515
            row = dict(row)
516
            row_nr += 1
517
518
            # sid is just for referring the user back to row X in their
519
            # in put spreadsheet
520
            gridrow = {'sid': row['Samples']}
521
            del (row['Samples'])
522
523
            # We'll use this later to verify the number against selections
524
            if 'Total number of Analyses or Profiles' in row:
525
                nr_an = row['Total number of Analyses or Profiles']
526
                del (row['Total number of Analyses or Profiles'])
527
            else:
528
                nr_an = 0
529
            try:
530
                nr_an = int(nr_an)
531
            except ValueError:
532
                nr_an = 0
533
534
            # TODO this is ignored and is probably meant to serve some purpose.
535
            del (row['Price excl Tax'])
536
537
            # ContainerType - not part of sample or AR schema
538
            if 'ContainerType' in row:
539
                title = row['ContainerType']
540
                if title:
541
                    obj = self.lookup(('ContainerType',),
542
                                      Title=row['ContainerType'])
543
                    if obj:
544
                        gridrow['ContainerType'] = obj[0].UID
545
                del (row['ContainerType'])
546
547
            if 'SampleMatrix' in row:
548
                # SampleMatrix - not part of sample or AR schema
549
                title = row['SampleMatrix']
550
                if title:
551
                    obj = self.lookup(('SampleMatrix',),
552
                                      Title=row['SampleMatrix'])
553
                    if obj:
554
                        gridrow['SampleMatrix'] = obj[0].UID
555
                del (row['SampleMatrix'])
556
557
            # match against sample schema
558
            for k, v in row.items():
559
                if k in ['Analyses', 'Profiles']:
560
                    continue
561
                if k in sample_schema:
562
                    del (row[k])
563
                    if v:
564
                        try:
565
                            value = self.munge_field_value(
566
                                sample_schema, row_nr, k, v)
567
                            gridrow[k] = value
568
                        except ValueError as e:
569
                            errors.append(e.message)
570
571
            # match against ar schema
572
            for k, v in row.items():
573
                if k in ['Analyses', 'Profiles']:
574
                    continue
575
                if k in ar_schema:
576
                    del (row[k])
577
                    if v:
578
                        try:
579
                            value = self.munge_field_value(
580
                                ar_schema, row_nr, k, v)
581
                            gridrow[k] = value
582
                        except ValueError as e:
583
                            errors.append(e.message)
584
585
            # Count and remove Keywords and Profiles from the list
586
            gridrow['Analyses'] = []
587
            for k, v in row.items():
588
                if k in keywords:
589
                    del (row[k])
590
                    if str(v).strip().lower() not in ('', '0', 'false'):
591
                        gridrow['Analyses'].append(k)
592
            gridrow['Profiles'] = []
593
            for k, v in row.items():
594
                if k in profiles:
595
                    del (row[k])
596
                    if str(v).strip().lower() not in ('', '0', 'false'):
597
                        gridrow['Profiles'].append(k)
598
            if len(gridrow['Analyses']) + len(gridrow['Profiles']) != nr_an:
599
                errors.append(
600
                    "Row %s: Number of analyses does not match provided value" %
601
                    row_nr)
602
603
            grid_rows.append(gridrow)
604
605
        self.setSampleData(grid_rows)
606
607
        if missing:
608
            self.error("SAMPLES: Missing expected fields: %s" %
609
                       ','.join(missing))
610
611
        for thing in errors:
612
            self.error(thing)
613
614
        if unexpected:
615
            self.error("Unexpected header fields: %s" %
616
                       ','.join(unexpected))
617
618
    def get_batch_header_values(self):
619
        """Scrape the "Batch Header" values from the original input file
620
        """
621
        lines = self.getOriginalFile().data.splitlines()
622
        reader = csv.reader(lines)
623
        batch_headers = batch_data = []
624
        for row in reader:
625
            if not any(row):
626
                continue
627
            if row[0].strip().lower() == 'batch header':
628
                batch_headers = [x.strip() for x in row][1:]
629
                continue
630
            if row[0].strip().lower() == 'batch data':
631
                batch_data = [x.strip() for x in row][1:]
632
                break
633
        if not (batch_data or batch_headers):
634
            return None
635
        if not (batch_data and batch_headers):
636
            self.error("Missing batch headers or data")
637
            return None
638
        # Inject us out of here
639
        values = dict(zip(batch_headers, batch_data))
640
        return values
641
642
    def create_or_reference_batch(self):
643
        """Save reference to batch, if existing batch specified
644
        Create new batch, if possible with specified values
645
        """
646
        client = self.aq_parent
647
        batch_headers = self.get_batch_header_values()
648
        if not batch_headers:
649
            return False
650
        # if the Batch's Title is specified and exists, no further
651
        # action is required. We will just set the Batch field to
652
        # use the existing object.
653
        batch_title = batch_headers.get('title', False)
654
        if batch_title:
655
            existing_batch = [x for x in client.objectValues('Batch')
656
                              if x.title == batch_title]
657
            if existing_batch:
658
                self.setBatch(existing_batch[0])
659
                return existing_batch[0]
660
        # If the batch title is specified but does not exist,
661
        # we will attempt to create the bach now.
662
        if 'title' in batch_headers:
663
            if 'id' in batch_headers:
664
                del (batch_headers['id'])
665
            if '' in batch_headers:
666
                del (batch_headers[''])
667
            batch = _createObjectByType('Batch', client, tmpID())
668
            batch.processForm()
669
            batch.edit(**batch_headers)
670
            self.setBatch(batch)
671
672
    def munge_field_value(self, schema, row_nr, fieldname, value):
673
        """Convert a spreadsheet value into a field value that fits in
674
        the corresponding schema field.
675
        - boolean: All values are true except '', 'false', or '0'.
676
        - reference: The title of an object in field.allowed_types;
677
            returns a UID or list of UIDs
678
        - datetime: returns a string value from ulocalized_time
679
680
        Tho this is only used during "Saving" of csv data into schema fields,
681
        it will flag 'validation' errors, as this is the only chance we will
682
        get to complain about these field values.
683
684
        """
685
        field = schema[fieldname]
686
        if field.type == 'boolean':
687
            value = str(value).strip().lower()
688
            value = '' if value in ['0', 'no', 'false', 'none'] else '1'
689
            return value
690 View Code Duplication
        if field.type == 'reference':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
691
            value = str(value).strip()
692
            brains = self.lookup(field.allowed_types, Title=value)
693
            if not brains:
694
                brains = self.lookup(field.allowed_types, UID=value)
695
            if not brains:
696
                raise ValueError('Row %s: value is invalid (%s=%s)' % (
697
                    row_nr, fieldname, value))
698
            if field.multiValued:
699
                return [b.UID for b in brains] if brains else []
700
            else:
701
                return brains[0].UID if brains else None
702
        if field.type == 'datetime':
703
            try:
704
                value = DateTime(value)
705
                return ulocalized_time(
706
                    value, long_format=True, time_only=False, context=self)
707
            except:
708
                raise ValueError('Row %s: value is invalid (%s=%s)' % (
709
                    row_nr, fieldname, value))
710
        return str(value)
711
712
    def validate_headers(self):
713
        """Validate headers fields from schema
714
        """
715
716
        pc = getToolByName(self, 'portal_catalog')
717
        pu = getToolByName(self, "plone_utils")
718
719
        client = self.aq_parent
720
721
        # Verify Client Name
722
        if self.getClientName() != client.Title():
723
            self.error("%s: value is invalid (%s)." % (
724
                'Client name', self.getClientName()))
725
726
        # Verify Client ID
727
        if self.getClientID() != client.getClientID():
728
            self.error("%s: value is invalid (%s)." % (
729
                'Client ID', self.getClientID()))
730
731
        existing_arimports = pc(portal_type='ARImport',
732
                                review_state=['valid', 'imported'])
733
        # Verify Client Order Number
734
        for arimport in existing_arimports:
735
            if arimport.UID == self.UID() \
736
                    or not arimport.getClientOrderNumber():
737
                continue
738
            arimport = arimport.getObject()
739
740
            if arimport.getClientOrderNumber() == self.getClientOrderNumber():
741
                self.error('%s: already used by existing ARImport.' %
742
                           'ClientOrderNumber')
743
                break
744
745
        # Verify Client Reference
746
        for arimport in existing_arimports:
747
            if arimport.UID == self.UID() \
748
                    or not arimport.getClientReference():
749
                continue
750
            arimport = arimport.getObject()
751
            if arimport.getClientReference() == self.getClientReference():
752
                self.error('%s: already used by existing ARImport.' %
753
                           'ClientReference')
754
                break
755
756
        # getCCContacts has no value if object is not complete (eg during test)
757
        if self.getCCContacts():
758
            cc_contacts = self.getCCContacts()[0]
759
            contacts = [x for x in client.objectValues('Contact')]
760
            contact_names = [c.Title() for c in contacts]
761
            # validate Contact existence in this Client
762
            for k in ['CCNamesReport', 'CCNamesInvoice']:
763
                for val in cc_contacts[k]:
764
                    if val and val not in contact_names:
765
                        self.error('%s: value is invalid (%s)' % (k, val))
766
        else:
767
            cc_contacts = {'CCNamesReport': [],
768
                           'CCEmailsReport': [],
769
                           'CCNamesInvoice': [],
770
                           'CCEmailsInvoice': []
771
                           }
772
            # validate Contact existence in this Client
773
            for k in ['CCEmailsReport', 'CCEmailsInvoice']:
774
                for val in cc_contacts.get(k, []):
775
                    if val and not pu.validateSingleNormalizedEmailAddress(val):
776
                        self.error('%s: value is invalid (%s)' % (k, val))
777
778
    def validate_samples(self):
779
        """Scan through the SampleData values and make sure
780
        that each one is correct
781
        """
782
783
        bsc = getToolByName(self, 'bika_setup_catalog')
784
        keywords = bsc.uniqueValuesFor('getKeyword')
785
        profiles = []
786
        for p in bsc(portal_type='AnalysisProfile'):
787
            p = p.getObject()
788
            profiles.append(p.Title())
789
            profiles.append(p.getProfileKey())
790
791
        row_nr = 0
792
        for gridrow in self.getSampleData():
793
            row_nr += 1
794
795
            # validate against sample and ar schemas
796
            for k, v in gridrow.items():
797
                if k in ['Analysis', 'Profiles']:
798
                    break
799
                if k in sample_schema:
800
                    try:
801
                        self.validate_against_schema(
802
                            sample_schema, row_nr, k, v)
803
                        continue
804
                    except ValueError as e:
805
                        self.error(e.message)
806
                        break
807
                if k in ar_schema:
808
                    try:
809
                        self.validate_against_schema(
810
                            ar_schema, row_nr, k, v)
811
                    except ValueError as e:
812
                        self.error(e.message)
813
814
            an_cnt = 0
815
            for v in gridrow['Analyses']:
816
                if v and v not in keywords:
817
                    self.error("Row %s: value is invalid (%s=%s)" %
818
                               ('Analysis keyword', row_nr, v))
819
                else:
820
                    an_cnt += 1
821
            for v in gridrow['Profiles']:
822
                if v and v not in profiles:
823
                    self.error("Row %s: value is invalid (%s=%s)" %
824
                               ('Profile Title', row_nr, v))
825
                else:
826
                    an_cnt += 1
827
            if not an_cnt:
828
                self.error("Row %s: No valid analyses or profiles" % row_nr)
829
830
    def validate_against_schema(self, schema, row_nr, fieldname, value):
831
        """
832
        """
833
        field = schema[fieldname]
834
        if field.type == 'boolean':
835
            value = str(value).strip().lower()
836
            return value
837 View Code Duplication
        if field.type == 'reference':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
838
            value = str(value).strip()
839
            if field.required and not value:
840
                raise ValueError("Row %s: %s field requires a value" % (
841
                    row_nr, fieldname))
842
            if not value:
843
                return value
844
            brains = self.lookup(field.allowed_types, UID=value)
845
            if not brains:
846
                raise ValueError("Row %s: value is invalid (%s=%s)" % (
847
                    row_nr, fieldname, value))
848
            if field.multiValued:
849
                return [b.UID for b in brains] if brains else []
850
            else:
851
                return brains[0].UID if brains else None
852
        if field.type == 'datetime':
853
            try:
854
                ulocalized_time(DateTime(value), long_format=True,
855
                                time_only=False, context=self)
856
            except:
857
                raise ValueError('Row %s: value is invalid (%s=%s)' % (
858
                    row_nr, fieldname, value))
859
        return value
860
861
    def lookup(self, allowed_types, **kwargs):
862
        """Lookup an object of type (allowed_types).  kwargs is sent
863
        directly to the catalog.
864
        """
865
        at = getToolByName(self, 'archetype_tool')
866
        for portal_type in allowed_types:
867
            catalog = at.catalog_map.get(portal_type, [None])[0]
868
            catalog = getToolByName(self, catalog)
869
            kwargs['portal_type'] = portal_type
870
            brains = catalog(**kwargs)
871
            if brains:
872
                return brains
873
874
    def get_row_services(self, row):
875
        """Return a list of services which are referenced in Analyses.
876
        values may be UID, Title or Keyword.
877
        """
878
        bsc = getToolByName(self, 'bika_setup_catalog')
879
        services = set()
880
        for val in row.get('Analyses', []):
881
            brains = bsc(portal_type='AnalysisService', getKeyword=val)
882
            if not brains:
883
                brains = bsc(portal_type='AnalysisService', title=val)
884
            if not brains:
885
                brains = bsc(portal_type='AnalysisService', UID=val)
886
            if brains:
887
                services.add(brains[0].UID)
888
            else:
889
                self.error("Invalid analysis specified: %s" % val)
890
        return list(services)
891
892
    def get_row_profile_services(self, row):
893
        """Return a list of services which are referenced in profiles
894
        values may be UID, Title or ProfileKey.
895
        """
896
        bsc = getToolByName(self, 'bika_setup_catalog')
897
        services = set()
898
        profiles = [x.getObject() for x in bsc(portal_type='AnalysisProfile')]
899
        for val in row.get('Profiles', []):
900
            objects = [x for x in profiles
901
                       if val in (x.getProfileKey(), x.UID(), x.Title())]
902
            if objects:
903
                for service in objects[0].getService():
904
                    services.add(service.UID())
905
            else:
906
                self.error("Invalid profile specified: %s" % val)
907
        return list(services)
908
909
    def get_row_container(self, row):
910
        """Return a sample container
911
        """
912
        bsc = getToolByName(self, 'bika_setup_catalog')
913
        val = row.get('Container', False)
914
        if val:
915
            brains = bsc(portal_type='Container', UID=row['Container'])
916
            if brains:
917
                brains[0].getObject()
918
            brains = bsc(portal_type='ContainerType', UID=row['Container'])
919
            if brains:
920
                # XXX Cheating.  The calculation of capacity vs. volume  is not done.
921
                return brains[0].getObject()
922
        return None
923
924
    def get_row_profiles(self, row):
925
        bsc = getToolByName(self, 'bika_setup_catalog')
926
        profiles = []
927
        for profile_title in row.get('Profiles', []):
928
            profile = bsc(portal_type='AnalysisProfile', title=profile_title)
929
            profiles.append(profile)
930
        return profiles
931
932
    def Vocabulary_SamplePoint(self):
933
        vocabulary = CatalogVocabulary(self)
934
        vocabulary.catalog = 'bika_setup_catalog'
935
        folders = [self.bika_setup.bika_samplepoints]
936
        if IClient.providedBy(self.aq_parent):
937
            folders.append(self.aq_parent)
938
        return vocabulary(allow_blank=True, portal_type='SamplePoint')
939
940
    def Vocabulary_SampleMatrix(self):
941
        vocabulary = CatalogVocabulary(self)
942
        vocabulary.catalog = 'bika_setup_catalog'
943
        return vocabulary(allow_blank=True, portal_type='SampleMatrix')
944
945
    def Vocabulary_SampleType(self):
946
        vocabulary = CatalogVocabulary(self)
947
        vocabulary.catalog = 'bika_setup_catalog'
948
        folders = [self.bika_setup.bika_sampletypes]
949
        if IClient.providedBy(self.aq_parent):
950
            folders.append(self.aq_parent)
951
        return vocabulary(allow_blank=True, portal_type='SampleType')
952
953
    def Vocabulary_ContainerType(self):
954
        vocabulary = CatalogVocabulary(self)
955
        vocabulary.catalog = 'bika_setup_catalog'
956
        return vocabulary(allow_blank=True, portal_type='ContainerType')
957
958
    def error(self, msg):
959
        errors = list(self.getErrors())
960
        errors.append(msg)
961
        self.setErrors(errors)
962
963
964
atapi.registerType(ARImport, PROJECTNAME)
965