Passed
Pull Request — 2.x (#1857)
by Ramon
08:37 queued 03:08
created

bika.lims.validators   F

Complexity

Total Complexity 218

Size/Duplication

Total Lines 1486
Duplicated Lines 10.16 %

Importance

Changes 0
Metric Value
wmc 218
eloc 918
dl 151
loc 1486
rs 1.682
c 0
b 0
f 0

How to fix   Duplicated Code    Complexity   

Duplicated Code

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:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like bika.lims.validators 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-2021 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import re
22
import string
23
import types
24
from time import strptime as _strptime
25
26
from bika.lims import api
27
from bika.lims import bikaMessageFactory as _
28
from bika.lims import logger
29
from bika.lims.api import APIError
30
from bika.lims.catalog import SETUP_CATALOG
31
from bika.lims.utils import t as _t
32
from bika.lims.utils import to_utf8
33
from Products.CMFCore.utils import getToolByName
34
from Products.CMFPlone.utils import safe_unicode
35
from Products.validation import validation
36
from Products.validation.interfaces.IValidator import IValidator
37
from Products.ZCTextIndex.ParseTree import ParseError
38
from zope.interface import implements
39
40
41
class IdentifierTypeAttributesValidator:
42
    """Validate IdentifierTypeAttributes to ensure that attributes are
43
    not duplicated.
44
    """
45
46
    implements(IValidator)
47
    name = "identifiertypeattributesvalidator"
48
49
    def __call__(self, value, *args, **kwargs):
50
        instance = kwargs['instance']
51
        request = instance.REQUEST
52
        form = request.get('form', {})
53
        fieldname = kwargs['field'].getName()
54
        form_value = form.get(fieldname, False)
55
        if form_value is False:
56
            # not required...
57
            return True
58
        if value == instance.get(fieldname):
59
            # no change.
60
            return True
61
62
        return True
63
64
65
validation.register(IdentifierTypeAttributesValidator())
66
67
68
class IdentifierValidator:
69
    """some actual validation should go here.
70
    I'm leaving this stub registered, but adding no extra validation.
71
    """
72
73
    implements(IValidator)
74
    name = "identifiervalidator"
75
76
    def __call__(self, value, *args, **kwargs):
77
        instance = kwargs['instance']
78
        request = instance.REQUEST
79
        form = request.get('form', {})
80
        fieldname = kwargs['field'].getName()
81
        form_value = form.get(fieldname, False)
82
        if form_value is False:
83
            # not required...
84
            return True
85
        if value == instance.get(fieldname):
86
            # no change.
87
            return True
88
89
        return True
90
91
92
validation.register(IdentifierValidator())
93
94
95
class UniqueFieldValidator:
96
    """Verifies if a field value is unique within the same container
97
    """
98
    implements(IValidator)
99
    name = "uniquefieldvalidator"
100
101
    def get_parent_objects(self, context):
102
        """Return all objects of the same type from the parent object
103
        """
104
        parent_object = api.get_parent(context)
105
        portal_type = api.get_portal_type(context)
106
        return parent_object.objectValues(portal_type)
107
108
    def query_parent_objects(self, context, query=None):
109
        """Return the objects of the same type from the parent object
110
111
        :param query: Catalog query to narrow down the objects
112
        :type query: dict
113
        :returns: Content objects of the same portal type in the parent
114
        """
115
116
        # return the object values if we have no catalog query
117
        if query is None:
118
            return self.get_parent_objects(context)
119
120
        # avoid undefined reference of catalog in except...
121
        catalog = None
122
123
        # try to fetch the results via the catalog
124
        try:
125
            catalogs = api.get_catalogs_for(context)
126
            catalog = catalogs[0]
127
            return map(api.get_object, catalog(query))
128
        except (IndexError, UnicodeDecodeError, ParseError, APIError) as e:
129
            # fall back to the object values of the parent
130
            logger.warn("UniqueFieldValidator: Catalog query {} failed "
131
                        "for catalog {} ({}) -> returning object values of {}"
132
                        .format(query, repr(catalog), str(e),
133
                                repr(api.get_parent(context))))
134
            return self.get_parent_objects(context)
135
136
    def make_catalog_query(self, context, field, value):
137
        """Create a catalog query for the field
138
        """
139
140
        # get the catalogs for the context
141
        catalogs = api.get_catalogs_for(context)
142
        # context not in any catalog?
143
        if not catalogs:
144
            logger.warn("UniqueFieldValidator: Context '{}' is not assigned"
145
                        "to any catalog!".format(repr(context)))
146
            return None
147
148
        # take the first catalog
149
        catalog = catalogs[0]
150
151
        # Check if the field accessor is indexed
152
        field_index = field.getName()
153
        accessor = field.getAccessor(context)
154
        if accessor:
155
            field_index = accessor.__name__
156
157
        # return if the field is not indexed
158
        if field_index not in catalog.indexes():
159
            return None
160
161
        # build a catalog query
162
        query = {
163
            "portal_type": api.get_portal_type(context),
164
            "path": {
165
                "query": api.get_parent_path(context),
166
                "depth": 1,
167
            }
168
        }
169
        query[field_index] = value
170
        logger.info("UniqueFieldValidator:Query={}".format(query))
171
        return query
172
173
    def __call__(self, value, *args, **kwargs):
174
        context = kwargs['instance']
175
        uid = api.get_uid(context)
176
        field = kwargs['field']
177
        fieldname = field.getName()
178
        translate = getToolByName(context, 'translation_service').translate
179
180
        # return directly if nothing changed
181
        if value == field.get(context):
182
            return True
183
184
        # Fetch the parent object candidates by catalog or by objectValues
185
        #
186
        # N.B. We want to use the catalog to speed things up, because using
187
        # `parent.objectValues` is very expensive if the parent object contains
188
        # many items and causes the UI to block too long
189
        catalog_query = self.make_catalog_query(context, field, value)
190
        parent_objects = self.query_parent_objects(
191
            context, query=catalog_query)
192
193
        for item in parent_objects:
194
            if hasattr(item, 'UID') and item.UID() != uid and \
195
               fieldname in item.Schema() and \
196
               str(item.Schema()[fieldname].get(item)) == str(value).strip():
197
                # We have to compare them as strings because
198
                # even if a number (as an  id) is saved inside
199
                # a string widget and string field, it will be
200
                # returned as an int. I don't know if it is
201
                # caused because is called with
202
                # <item.Schema()[fieldname].get(item)>,
203
                # but it happens...
204
                msg = _(
205
                    "Validation failed: '${value}' is not unique",
206
                    mapping={
207
                        'value': safe_unicode(value)
208
                    })
209
                return to_utf8(translate(msg))
210
        return True
211
212
213
validation.register(UniqueFieldValidator())
214
215
216
class InvoiceBatch_EndDate_Validator:
217
    """ Verifies that the End Date is after the Start Date """
218
219
    implements(IValidator)
220
    name = "invoicebatch_EndDate_validator"
221
222
    def __call__(self, value, *args, **kwargs):
223
        instance = kwargs.get('instance')
224
        request = kwargs.get('REQUEST')
225
226
        if request and request.form.get('BatchStartDate'):
227
            startdate = _strptime(request.form.get('BatchStartDate'), '%Y-%m-%d %H:%M')
228
        else:
229
            startdate = _strptime(instance.getBatchStartDate(), '%Y-%m-%d %H:%M')
230
231
        enddate = _strptime(value, '%Y-%m-%d %H:%M')
232
233
        translate = api.get_tool('translation_service', instance).translate
234
        if not enddate >= startdate:
235
            msg = _("Start date must be before End Date")
236
            return to_utf8(translate(msg))
237
        return True
238
239
240
validation.register(InvoiceBatch_EndDate_Validator())
241
242
243
class ServiceKeywordValidator:
244
    """Validate AnalysisService Keywords
245
    must match isUnixLikeName
246
    may not be the same as another service keyword
247
    may not be the same as any InterimField id.
248
    """
249
250
    implements(IValidator)
251
    name = "servicekeywordvalidator"
252
253
    def __call__(self, value, *args, **kwargs):
254
        instance = kwargs['instance']
255
        # fieldname = kwargs['field'].getName()
256
        # request = kwargs.get('REQUEST', {})
257
        # form = request.get('form', {})
258
259
        translate = getToolByName(instance, 'translation_service').translate
260
261
        if re.findall(r"[^A-Za-z\w\d\-\_]", value):
262
            return _("Validation failed: keyword contains invalid characters")
263
264
        # check the value against all AnalysisService keywords
265
        # this has to be done from catalog so we don't
266
        # clash with ourself
267
        bsc = getToolByName(instance, 'bika_setup_catalog')
268
        services = bsc(portal_type='AnalysisService', getKeyword=value)
269
        for service in services:
270
            if service.UID != instance.UID():
271
                msg = _(
272
                    "Validation failed: '${title}': This keyword "
273
                    "is already in use by service '${used_by}'",
274
                    mapping={
275
                        'title': safe_unicode(value),
276
                        'used_by': safe_unicode(service.Title)
277
                    })
278
                return to_utf8(translate(msg))
279
280
        calc = hasattr(instance, 'getCalculation') and \
281
            instance.getCalculation() or None
282
        our_calc_uid = calc and calc.UID() or ''
283
284
        # check the value against all Calculation Interim Field ids
285
        calcs = [c for c in bsc(portal_type='Calculation')]
286
        for calc in calcs:
287
            calc = calc.getObject()
288
            interim_fields = calc.getInterimFields()
289
            if not interim_fields:
290
                continue
291
            for field in interim_fields:
292
                if field['keyword'] == value and our_calc_uid != calc.UID():
293
                    msg = _(
294
                        "Validation failed: '${title}': This keyword "
295
                        "is already in use by calculation '${used_by}'",
296
                        mapping={
297
                            'title': safe_unicode(value),
298
                            'used_by': safe_unicode(calc.Title())
299
                        })
300
                    return to_utf8(translate(msg))
301
        return True
302
303
304
validation.register(ServiceKeywordValidator())
305
306
307
class InterimFieldsValidator:
308
    """Validating InterimField keywords.
309
        XXX Applied as a subfield validator but validates entire field.
310
        keyword must match isUnixLikeName
311
        keyword may not be the same as any service keyword.
312
        keyword must be unique in this InterimFields field
313
        keyword must be unique for interimfields which share the same title.
314
        title must be unique for interimfields which share the same keyword.
315
    """
316
317
    implements(IValidator)
318
    name = "interimfieldsvalidator"
319
320
    def __call__(self, value, *args, **kwargs):
321
        instance = kwargs['instance']
322
        fieldname = kwargs['field'].getName()
323
        request = kwargs.get('REQUEST', {})
324
        form = request.form
325
        interim_fields = form.get(fieldname, [])
326
327
        translate = getToolByName(instance, 'translation_service').translate
328
        bsc = getToolByName(instance, 'bika_setup_catalog')
329
330
        # We run through the validator once per form submit, and check all
331
        # values
332
        # this value in request prevents running once per subfield value.
333
        key = instance.id + fieldname
334
        if instance.REQUEST.get(key, False):
335
            return True
336
337
        for x in range(len(interim_fields)):
338
            row = interim_fields[x]
339
            keys = row.keys()
340
            if 'title' not in keys:
341
                instance.REQUEST[key] = to_utf8(
342
                    translate(_("Validation failed: title is required")))
343
                return instance.REQUEST[key]
344
            if 'keyword' not in keys:
345
                instance.REQUEST[key] = to_utf8(
346
                    translate(_("Validation failed: keyword is required")))
347
                return instance.REQUEST[key]
348
            if not re.match(r"^[A-Za-z\w\d\-\_]+$", row['keyword']):
349
                instance.REQUEST[key] = _(
350
                    "Validation failed: keyword contains invalid characters")
351
                return instance.REQUEST[key]
352
353
        # keywords and titles used once only in the submitted form
354
        keywords = {}
355
        titles = {}
356
        for field in interim_fields:
357
            if 'keyword' in field:
358
                if field['keyword'] in keywords:
359
                    keywords[field['keyword']] += 1
360
                else:
361
                    keywords[field['keyword']] = 1
362
            if 'title' in field:
363
                if field['title'] in titles:
364
                    titles[field['title']] += 1
365
                else:
366
                    titles[field['title']] = 1
367
        for k in [k for k in keywords.keys() if keywords[k] > 1]:
368
            msg = _(
369
                "Validation failed: '${keyword}': duplicate keyword",
370
                mapping={
371
                    'keyword': safe_unicode(k)
372
                })
373
            instance.REQUEST[key] = to_utf8(translate(msg))
374
            return instance.REQUEST[key]
375
        for t in [t for t in titles.keys() if titles[t] > 1]:
376
            msg = _(
377
                "Validation failed: '${title}': duplicate title",
378
                mapping={
379
                    'title': safe_unicode(t)
380
                })
381
            instance.REQUEST[key] = to_utf8(translate(msg))
382
            return instance.REQUEST[key]
383
384
        # check all keywords against all AnalysisService keywords for dups
385
        services = bsc(portal_type='AnalysisService', getKeyword=value)
386
        if services:
387
            msg = _(
388
                "Validation failed: '${title}': "
389
                "This keyword is already in use by service '${used_by}'",
390
                mapping={
391
                    'title': safe_unicode(value),
392
                    'used_by': safe_unicode(services[0].Title)
393
                })
394
            instance.REQUEST[key] = to_utf8(translate(msg))
395
            return instance.REQUEST[key]
396
397
        # any duplicated interimfield titles must share the same keyword
398
        # any duplicated interimfield keywords must share the same title
399
        calcs = bsc(portal_type='Calculation')
400
        keyword_titles = {}
401
        title_keywords = {}
402
        for calc in calcs:
403
            if calc.UID == instance.UID():
404
                continue
405
            calc = calc.getObject()
406
            for field in calc.getInterimFields():
407
                keyword_titles[field['keyword']] = field['title']
408
                title_keywords[field['title']] = field['keyword']
409
        for field in interim_fields:
410
            if field['keyword'] != value:
411
                continue
412
            if 'title' in field and \
413
               field['title'] in title_keywords.keys() and \
414
               title_keywords[field['title']] != field['keyword']:
415
                msg = _(
416
                    "Validation failed: column title '${title}' "
417
                    "must have keyword '${keyword}'",
418
                    mapping={
419
                        'title': safe_unicode(field['title']),
420
                        'keyword': safe_unicode(title_keywords[field['title']])
421
                    })
422
                instance.REQUEST[key] = to_utf8(translate(msg))
423
                return instance.REQUEST[key]
424
            if 'keyword' in field and \
425
               field['keyword'] in keyword_titles.keys() and \
426
               keyword_titles[field['keyword']] != field['title']:
427
                msg = _(
428
                    "Validation failed: keyword '${keyword}' "
429
                    "must have column title '${title}'",
430
                    mapping={
431
                        'keyword': safe_unicode(field['keyword']),
432
                        'title': safe_unicode(keyword_titles[field['keyword']])
433
                    })
434
                instance.REQUEST[key] = to_utf8(translate(msg))
435
                return instance.REQUEST[key]
436
437
        # Check if choices subfield is valid
438
        for interim in interim_fields:
439
            message = self.validate_choices(interim)
440
            if message:
441
                # Not a valid choice
442
                instance.REQUEST[key] = message
443
                return message
444
445
        instance.REQUEST[key] = True
446
        return True
447
448
    def validate_choices(self, interim):
449
        """Checks whether the choices are valid for the given interim
450
        """
451
        choices = interim.get("choices")
452
        if not choices:
453
            # No choices set, nothing to do
454
            return
455
456
        # Choices are expressed like "value0:text0|value1:text1|..|valuen:textn"
457
        choices = choices.split("|") or []
458
        try:
459
            choices = dict(map(lambda ch: ch.strip().split(":"), choices))
460
        except ValueError:
461
            return _t(_(
462
                "No valid format in choices field. Supported format is: "
463
                "<value-0>:<text>|<value-1>:<text>|<value-n>:<text>"))
464
465
        # Empty keys (that match with the result value) are not allowed
466
        keys = map(lambda k: k.strip(), choices.keys())
467
        empties = filter(None, keys)
468
        if len(empties) != len(keys):
469
            return _t(_("Empty keys are not supported"))
470
471
        # No duplicate keys allowed
472
        unique_keys = list(set(keys))
473
        if len(unique_keys) != len(keys):
474
            return _t(_("Duplicate keys in choices field"))
475
476
        # We need at least 2 choices
477
        if len(keys) < 2:
478
            return _t(_("At least, two options for choices field are required"))
479
480
481
validation.register(InterimFieldsValidator())
482
483
484
class FormulaValidator:
485
    """ Validate keywords in calculation formula entry
486
    """
487
    implements(IValidator)
488
    name = "formulavalidator"
489
490
    def __call__(self, value, *args, **kwargs):
491
        if not value:
492
            return True
493
        instance = kwargs["instance"]
494
        request = api.get_request()
495
        form = request.form
496
        interim_fields = form.get("InterimFields", [])
497
        translate = getToolByName(instance, "translation_service").translate
498
        catalog = api.get_tool(SETUP_CATALOG)
499
        interim_keywords = filter(None, map(
500
            lambda i: i.get("keyword"), interim_fields))
501
        keywords = re.compile(r"\[([^\.^\]]+)\]").findall(value)
502
503
        for keyword in keywords:
504
            # Check if the service keyword exists and is active.
505
            dep_service = catalog(getKeyword=keyword, is_active=True)
506
            if not dep_service and keyword not in interim_keywords:
507
                msg = _(
508
                    "Validation failed: Keyword '${keyword}' is invalid",
509
                    mapping={
510
                        'keyword': safe_unicode(keyword)
511
                    })
512
                return to_utf8(translate(msg))
513
514
        # Allow to use Wildcards, LDL and UDL values in calculations
515
        allowedwds = ["LDL", "UDL", "BELOWLDL", "ABOVEUDL"]
516
        keysandwildcards = re.compile(r"\[([^\]]+)\]").findall(value)
517
        keysandwildcards = [k for k in keysandwildcards if "." in k]
518
        keysandwildcards = [k.split(".", 1) for k in keysandwildcards]
519
        errwilds = [k[1] for k in keysandwildcards if k[0] not in keywords]
520
        if len(errwilds) > 0:
521
            msg = _(
522
                "Wildcards for interims are not allowed: ${wildcards}",
523
                mapping={
524
                    "wildcards": safe_unicode(", ".join(errwilds))
525
                })
526
            return to_utf8(translate(msg))
527
528
        wildcards = [k[1] for k in keysandwildcards if k[0] in keywords]
529
        wildcards = [wd for wd in wildcards if wd not in allowedwds]
530
        if len(wildcards) > 0:
531
            msg = _(
532
                "Invalid wildcards found: ${wildcards}",
533
                mapping={
534
                    "wildcards": safe_unicode(", ".join(wildcards))
535
                })
536
            return to_utf8(translate(msg))
537
538
        return True
539
540
541
validation.register(FormulaValidator())
542
543
544
class CoordinateValidator:
545
    """ Validate latitude or longitude field values
546
    """
547
    implements(IValidator)
548
    name = "coordinatevalidator"
549
550
    def __call__(self, value, **kwargs):
551
        if not value:
552
            return True
553
554
        instance = kwargs['instance']
555
        fieldname = kwargs['field'].getName()
556
        request = instance.REQUEST
557
558
        form = request.form
559
        form_value = form.get(fieldname)
560
561
        translate = getToolByName(instance, 'translation_service').translate
562
563
        try:
564
            degrees = int(form_value['degrees'])
565
        except ValueError:
566
            return to_utf8(
567
                translate(_("Validation failed: degrees must be numeric")))
568
569
        try:
570
            minutes = int(form_value['minutes'])
571
        except ValueError:
572
            return to_utf8(
573
                translate(_("Validation failed: minutes must be numeric")))
574
575
        try:
576
            seconds = int(form_value['seconds'])
577
        except ValueError:
578
            return to_utf8(
579
                translate(_("Validation failed: seconds must be numeric")))
580
581
        if not 0 <= minutes <= 59:
582
            return to_utf8(
583
                translate(_("Validation failed: minutes must be 0 - 59")))
584
585
        if not 0 <= seconds <= 59:
586
            return to_utf8(
587
                translate(_("Validation failed: seconds must be 0 - 59")))
588
589
        bearing = form_value['bearing']
590
591
        if fieldname == 'Latitude':
592
            if not 0 <= degrees <= 90:
593
                return to_utf8(
594
                    translate(_("Validation failed: degrees must be 0 - 90")))
595
            if degrees == 90:
596
                if minutes != 0:
597
                    return to_utf8(
598
                        translate(
599
                            _("Validation failed: degrees is 90; "
600
                              "minutes must be zero")))
601
                if seconds != 0:
602
                    return to_utf8(
603
                        translate(
604
                            _("Validation failed: degrees is 90; "
605
                              "seconds must be zero")))
606
            if bearing.lower() not in 'sn':
607
                return to_utf8(
608
                    translate(_("Validation failed: Bearing must be N/S")))
609
610
        if fieldname == 'Longitude':
611
            if not 0 <= degrees <= 180:
612
                return to_utf8(
613
                    translate(_("Validation failed: degrees must be 0 - 180")))
614
            if degrees == 180:
615
                if minutes != 0:
616
                    return to_utf8(
617
                        translate(
618
                            _("Validation failed: degrees is 180; "
619
                              "minutes must be zero")))
620
                if seconds != 0:
621
                    return to_utf8(
622
                        translate(
623
                            _("Validation failed: degrees is 180; "
624
                              "seconds must be zero")))
625
            if bearing.lower() not in 'ew':
626
                return to_utf8(
627
                    translate(_("Validation failed: Bearing must be E/W")))
628
629
        return True
630
631
632
validation.register(CoordinateValidator())
633
634
635
class ResultOptionsValueValidator(object):
636
    """Validator for the subfield "ResultValue" of ResultOptions field
637
    """
638
639
    implements(IValidator)
640
    name = "result_options_value_validator"
641
642
    def __call__(self, value, *args, **kwargs):
643
        # Result Value must be floatable
644
        if not api.is_floatable(value):
645
            return _t(_("Result Value must be a number"))
646
647
        # Get all records
648
        instance = kwargs['instance']
649
        field_name = kwargs['field'].getName()
650
        request = instance.REQUEST
651
        records = request.form.get(field_name)
652
653
        # Result values must be unique
654
        value = api.to_float(value)
655
        values = map(lambda ro: ro.get("ResultValue"), records)
656
        values = filter(api.is_floatable, values)
657
        values = map(api.to_float, values)
658
        duplicates = filter(lambda val: val == value, values)
659
        if len(duplicates) > 1:
660
            return _t(_("Result Value must be unique"))
661
662
        return True
663
664
665
validation.register(ResultOptionsValueValidator())
666
667
668
class ResultOptionsTextValidator(object):
669
    """Validator for the subfield "ResultText" of ResultsOption field
670
    """
671
672
    implements(IValidator)
673
    name = "result_options_text_validator"
674
675
    def __call__(self, value, *args, **kwargs):
676
        # Result Text is required
677
        if not value or not value.strip():
678
            return _t(_("Display Value is required"))
679
680
        # Get all records
681
        instance = kwargs['instance']
682
        field_name = kwargs['field'].getName()
683
        request = instance.REQUEST
684
        records = request.form.get(field_name)
685
686
        # Result Text must be unique
687
        original_texts = map(lambda ro: ro.get("ResultText"), records)
688
        duplicates = filter(lambda text: text == value, original_texts)
689
        if len(duplicates) > 1:
690
            return _t(_("Display Value must be unique"))
691
692
        return True
693
694
695
validation.register(ResultOptionsTextValidator())
696
697
698
class RestrictedCategoriesValidator:
699
    """ Verifies that client Restricted categories include all categories
700
    required by service dependencies. """
701
702
    implements(IValidator)
703
    name = "restrictedcategoriesvalidator"
704
705
    def __call__(self, value, *args, **kwargs):
706
        instance = kwargs['instance']
707
        # fieldname = kwargs['field'].getName()
708
        # request = kwargs.get('REQUEST', {})
709
        # form = request.get('form', {})
710
711
        translate = getToolByName(instance, 'translation_service').translate
712
        bsc = getToolByName(instance, 'bika_setup_catalog')
713
        # uc = getToolByName(instance, 'uid_catalog')
714
715
        failures = []
716
717
        for category in value:
718
            if not category:
719
                continue
720
            services = bsc(portal_type="AnalysisService", category_uid=category)
721
            for service in services:
722
                service = service.getObject()
723
                calc = service.getCalculation()
724
                deps = calc and calc.getDependentServices() or []
725
                for dep in deps:
726
                    if dep.getCategoryUID() not in value:
727
                        title = dep.getCategoryTitle()
728
                        if title not in failures:
729
                            failures.append(title)
730
        if failures:
731
            msg = _(
732
                "Validation failed: The selection requires the following "
733
                "categories to be selected: ${categories}",
734
                mapping={
735
                    'categories': safe_unicode(','.join(failures))
736
                })
737
            return to_utf8(translate(msg))
738
739
        return True
740
741
742
validation.register(RestrictedCategoriesValidator())
743
744
745
class PrePreservationValidator:
746
    """ Validate PrePreserved Containers.
747
        User must select a Preservation.
748
    """
749
    implements(IValidator)
750
    name = "container_prepreservation_validator"
751
752
    def __call__(self, value, *args, **kwargs):
753
        # If not prepreserved, no validation required.
754
        if not value:
755
            return True
756
757
        instance = kwargs['instance']
758
        # fieldname = kwargs['field'].getName()
759
        request = kwargs.get('REQUEST', {})
760
        form = request.form
761
        preservation = form.get('Preservation')
762
763
        if type(preservation) in (list, tuple):
764
            preservation = preservation[0]
765
766
        if preservation:
767
            return True
768
769
        translate = getToolByName(instance, 'translation_service').translate
770
        # bsc = getToolByName(instance, 'bika_setup_catalog')
771
772
        if not preservation:
773
            msg = _("Validation failed: PrePreserved containers "
774
                    "must have a preservation selected.")
775
            return to_utf8(translate(msg))
776
777
778
validation.register(PrePreservationValidator())
779
780
781
class StandardIDValidator:
782
    """Matches against regular expression:
783
       [^A-Za-z\w\d\-\_]
784
    """
785
786
    implements(IValidator)
787
    name = "standard_id_validator"
788
789
    def __call__(self, value, *args, **kwargs):
790
791
        regex = r"[^A-Za-z\w\d\-\_]"
792
793
        instance = kwargs['instance']
794
        # fieldname = kwargs['field'].getName()
795
        # request = kwargs.get('REQUEST', {})
796
        # form = request.get('form', {})
797
798
        translate = getToolByName(instance, 'translation_service').translate
799
800
        # check the value against all AnalysisService keywords
801
        if re.findall(regex, value):
802
            msg = _("Validation failed: keyword contains invalid "
803
                    "characters")
804
            return to_utf8(translate(msg))
805
806
        return True
807
808
809
validation.register(StandardIDValidator())
810
811
812
def get_record_value(request, uid, keyword, default=None):
813
    """Returns the value for the keyword and uid from the request"""
814
    value = request.get(keyword)
815
    if not value:
816
        return default
817
    if not isinstance(value, list):
818
        return default
819
    return value[0].get(uid, default) or default
820
821
822
class AnalysisSpecificationsValidator:
823
    """Min value must be below max value
824
       Warn min value must be below min value or empty
825
       Warn max value must above max value or empty
826
       Percentage value must be between 0 and 100
827
       Values must be numbers
828
    """
829
830
    implements(IValidator)
831
    name = "analysisspecs_validator"
832
833
    def __call__(self, value, *args, **kwargs):
834
        instance = kwargs['instance']
835
        request = kwargs.get('REQUEST', {})
836
        fieldname = kwargs['field'].getName()
837
838
        # This value in request prevents running once per subfield value.
839
        # self.name returns the name of the validator. This allows other
840
        # subfield validators to be called if defined (eg. in other add-ons)
841
        key = '{}-{}-{}'.format(self.name, instance.getId(), fieldname)
842
        if instance.REQUEST.get(key, False):
843
            return True
844
845
        # Walk through all AS UIDs and validate each parameter for that AS
846
        service_uids = request.get("uids", [])
847
        for uid in service_uids:
848
            err_msg = self.validate_service(request, uid)
849
            if not err_msg:
850
                continue
851
852
            # Validation failed
853
            service = api.get_object_by_uid(uid)
854
            title = api.get_title(service)
855
856
            err_msg = "{}: {}".format(title, _(err_msg))
857
            translate = api.get_tool('translation_service').translate
858
            instance.REQUEST[key] = to_utf8(translate(safe_unicode(err_msg)))
859
            return instance.REQUEST[key]
860
861
        instance.REQUEST[key] = True
862
        return True
863
864
    def validate_service(self, request, uid):
865
        """Validates the specs values from request for the service uid. Returns
866
        a non-translated message if the validation failed.
867
        """
868
        spec_min = get_record_value(request, uid, "min")
869
        spec_max = get_record_value(request, uid, "max")
870
        error = get_record_value(request, uid, "error", "0")
871
        warn_min = get_record_value(request, uid, "warn_min")
872
        warn_max = get_record_value(request, uid, "warn_max")
873
874
        if not spec_min and not spec_max:
875
            # Neither min nor max values have been set, dismiss
876
            return None
877
878
        if not api.is_floatable(spec_min):
879
            return "'Min' value must be numeric"
880
        if not api.is_floatable(spec_max):
881
            return "'Max' value must be numeric"
882
        if api.to_float(spec_min) > api.to_float(spec_max):
883
            return "'Max' value must be above 'Min' value"
884
        if not api.is_floatable(error) or 0.0 < api.to_float(error) > 100:
885
            return "% Error must be between 0 and 100"
886
887
        if warn_min:
888
            if not api.is_floatable(warn_min):
889
                return "'Warn Min' value must be numeric or empty"
890
            if api.to_float(warn_min) > api.to_float(spec_min):
891
                return "'Warn Min' value must be below 'Min' value"
892
893
        if warn_max:
894
            if not api.is_floatable(warn_max):
895
                return "'Warn Max' value must be numeric or empty"
896
            if api.to_float(warn_max) < api.to_float(spec_max):
897
                return "'Warn Max' value must be above 'Max' value"
898
        return None
899
900
901
validation.register(AnalysisSpecificationsValidator())
902
903
904
class UncertaintiesValidator:
905
    """Uncertainties may be specified as numeric values or percentages.
906
    Min value must be below max value.
907
    Uncertainty must not be < 0.
908
    """
909
910
    implements(IValidator)
911
    name = "uncertainties_validator"
912
913
    def __call__(self, subf_value, *args, **kwargs):
914
915
        instance = kwargs['instance']
916
        request = kwargs.get('REQUEST', {})
917
        fieldname = kwargs['field'].getName()
918
        translate = getToolByName(instance, 'translation_service').translate
919
920
        # We run through the validator once per form submit, and check all
921
        # values
922
        # this value in request prevents running once per subfield value.
923
        key = instance.id + fieldname
924
        if instance.REQUEST.get(key, False):
925
            return True
926
927
        for i, value in enumerate(request[fieldname]):
928
929
            # Values must be numbers
930
            try:
931
                minv = float(value['intercept_min'])
932
            except ValueError:
933
                instance.REQUEST[key] = to_utf8(
934
                    translate(
935
                        _("Validation failed: Min values must be numeric")))
936
                return instance.REQUEST[key]
937
            try:
938
                maxv = float(value['intercept_max'])
939
            except ValueError:
940
                instance.REQUEST[key] = to_utf8(
941
                    translate(
942
                        _("Validation failed: Max values must be numeric")))
943
                return instance.REQUEST[key]
944
945
            # values may be percentages; the rest of the numeric validation must
946
            # still pass once the '%' is stripped off.
947
            err = value['errorvalue']
948
            perc = False
949
            if err.endswith('%'):
950
                perc = True
951
                err = err[:-1]
952
            try:
953
                err = float(err)
954
            except ValueError:
955
                instance.REQUEST[key] = to_utf8(
956
                    translate(
957
                        _("Validation failed: Error values must be numeric")))
958
                return instance.REQUEST[key]
959
960
            if perc and (err < 0 or err > 100):
961
                # Error percentage must be between 0 and 100
962
                instance.REQUEST[key] = to_utf8(
963
                    translate(
964
                        _("Validation failed: Error percentage must be between 0 "
965
                          "and 100")))
966
                return instance.REQUEST[key]
967
968
            # Min value must be < max
969
            if minv > maxv:
970
                instance.REQUEST[key] = to_utf8(
971
                    translate(
972
                        _("Validation failed: Max values must be greater than Min "
973
                          "values")))
974
                return instance.REQUEST[key]
975
976
            # Error values must be >-1
977
            if err < 0:
978
                instance.REQUEST[key] = to_utf8(
979
                    translate(
980
                        _("Validation failed: Error value must be 0 or greater"
981
                          )))
982
                return instance.REQUEST[key]
983
984
        instance.REQUEST[key] = True
985
        return True
986
987
988
validation.register(UncertaintiesValidator())
989
990
991
class DurationValidator:
992
    """Simple stuff - just checking for integer values.
993
    """
994
995
    implements(IValidator)
996
    name = "duration_validator"
997
998
    def __call__(self, value, *args, **kwargs):
999
1000
        instance = kwargs['instance']
1001
        request = kwargs.get('REQUEST', {})
1002
        fieldname = kwargs['field'].getName()
1003
        translate = getToolByName(instance, 'translation_service').translate
1004
1005
        value = request[fieldname]
1006
        for v in value.values():
1007
            try:
1008
                int(v)
1009
            except:
1010
                return to_utf8(
1011
                    translate(_("Validation failed: Values must be numbers")))
1012
        return True
1013
1014
1015
validation.register(DurationValidator())
1016
1017
1018
class ReferenceValuesValidator:
1019
    """Min value must be below max value
1020
       Percentage value must be between 0 and 100
1021
       Values must be numbers
1022
       Expected values must be between min and max values
1023
    """
1024
1025
    implements(IValidator)
1026
    name = "referencevalues_validator"
1027
1028
    def __call__(self, value, *args, **kwargs):
1029
        request = kwargs.get('REQUEST', {})
1030
        # Retrieve all AS uids
1031
        services = request.get('service', [{}])[0]
1032
        for uid, service_name in services.items():
1033
            err_msg = self.validate_service(request, uid)
1034
            if not err_msg:
1035
                continue
1036
1037
            # Validation failed
1038
            err_msg = "{}: {}".format(_("Validation for '{}' failed"),
1039
                                      _(err_msg))
1040
            err_msg = err_msg.format(service_name)
1041
            translate = api.get_tool('translation_service').translate
1042
            return to_utf8(translate(safe_unicode(err_msg)))
1043
1044
        return True
1045
1046
    def validate_service(self, request, uid):
1047
        """Validates the specs values from request for the service uid. Returns
1048
        a non-translated message if the validation failed."""
1049
1050
        result = get_record_value(request, uid, 'result')
1051
        if not result:
1052
            # No result set for this service, dismiss
1053
            return None
1054
1055
        if not api.is_floatable(result):
1056
            return "Expected result value must be numeric"
1057
1058
        spec_min = get_record_value(request, uid, "min", result)
1059
        spec_max = get_record_value(request, uid, "max", result)
1060
        error = get_record_value(request, uid, "error", "0")
1061
        if not api.is_floatable(spec_min):
1062
            return "'Min' value must be numeric"
1063
        if not api.is_floatable(spec_max):
1064
            return "'Max' value must be numeric"
1065
        if api.to_float(spec_min) > api.to_float(result):
1066
            return "'Min' value must be below the expected result"
1067
        if api.to_float(spec_max) < api.to_float(result):
1068
            return "'Max' value must be above the expected result"
1069
        if not api.is_floatable(error) or 0.0 < api.to_float(error) > 100:
1070
            return "% Error must be between 0 and 100"
1071
        return None
1072
1073
validation.register(ReferenceValuesValidator())
1074
1075
1076
class PercentValidator:
1077
    """ Floatable, >=0, <=100. """
1078
1079
    implements(IValidator)
1080
    name = "percentvalidator"
1081
1082
    def __call__(self, value, *args, **kwargs):
1083
        instance = kwargs['instance']
1084
        # fieldname = kwargs['field'].getName()
1085
        # request = kwargs.get('REQUEST', {})
1086
        # form = request.get('form', {})
1087
1088
        translate = getToolByName(instance, 'translation_service').translate
1089
1090
        try:
1091
            value = float(value)
1092
        except:
1093
            msg = _("Validation failed: percent values must be numbers")
1094
            return to_utf8(translate(msg))
1095
1096
        if value < 0 or value > 100:
1097
            msg = _(
1098
                "Validation failed: percent values must be between 0 and 100")
1099
            return to_utf8(translate(msg))
1100
1101
        return True
1102
1103
1104
validation.register(PercentValidator())
1105
1106
1107
def _toIntList(numstr, acceptX=0):
1108
    """
1109
    Convert ans string to a list removing all invalid characters.
1110
    Receive: a string as a number
1111
    """
1112
    res = []
1113
    # Converting and removing invalid characters
1114
    for i in numstr:
1115
        if i in string.digits and i not in string.letters:
1116
            res.append(int(i))
1117
1118
    # Converting control number into ISBN
1119
    if acceptX and (numstr[-1] in 'Xx'):
1120
        res.append(10)
1121
    return res
1122
1123
1124
def _sumLists(a, b):
1125
    """
1126
    Algorithm to check validity of NBI and NIF.
1127
    Receives string with a umber to validate.
1128
    """
1129
    val = 0
1130
    for i in map(lambda a, b: a * b, a, b):
1131
        val += i
1132
    return val
1133
1134
1135
class NIBvalidator:
1136
    """
1137
    Validates if the introduced NIB is correct.
1138
    """
1139
1140
    implements(IValidator)
1141
    name = "NIBvalidator"
1142
1143
    def __call__(self, value, *args, **kwargs):
1144
        """
1145
        Check the NIB number
1146
        value:: string with NIB.
1147
        """
1148
        instance = kwargs['instance']
1149
        translate = getToolByName(instance, 'translation_service').translate
1150
        LEN_NIB = 21
1151
        table = (73, 17, 89, 38, 62, 45, 53, 15, 50, 5, 49, 34, 81, 76, 27, 90,
1152
                 9, 30, 3)
1153
1154
        # convert to entire numbers list
1155
        nib = _toIntList(value)
1156
1157
        # checking the length of the number
1158
        if len(nib) != LEN_NIB:
1159
            msg = _('Incorrect NIB number: %s' % value)
1160
            return to_utf8(translate(msg))
1161
        # last numbers algorithm validator
1162
        return nib[-2] * 10 + nib[-1] == 98 - _sumLists(table, nib[:-2]) % 97
1163
1164
1165
validation.register(NIBvalidator())
1166
1167
1168
class IBANvalidator:
1169
    """
1170
    Validates if the introduced NIB is correct.
1171
    """
1172
1173
    implements(IValidator)
1174
    name = "IBANvalidator"
1175
1176
    def __call__(self, value, *args, **kwargs):
1177
        instance = kwargs['instance']
1178
        translate = getToolByName(instance, 'translation_service').translate
1179
1180
        # remove spaces from formatted
1181
        IBAN = ''.join(c for c in value if c.isalnum())
1182
1183
        IBAN = IBAN[4:] + IBAN[:4]
1184
        country = IBAN[-4:-2]
1185
1186
        if country not in country_dic:
1187
            msg = _('Unknown IBAN country %s' % country)
1188
            return to_utf8(translate(msg))
1189
1190
        length_c, name_c = country_dic[country]
1191
1192
        if len(IBAN) != length_c:
1193
            diff = len(IBAN) - length_c
1194
            msg = _('Wrong IBAN length by %s: %s' %
1195
                    (('short by %i' % -diff)
1196
                     if diff < 0 else ('too long by %i' % diff), value))
1197
            return to_utf8(translate(msg))
1198
        # Validating procedure
1199
        elif int("".join(str(letter_dic[x]) for x in IBAN)) % 97 != 1:
1200
            msg = _('Incorrect IBAN number: %s' % value)
1201
            return to_utf8(translate(msg))
1202
1203
        else:
1204
            # Accepted:
1205
            return True
1206
1207
1208
validation.register(IBANvalidator())
1209
1210
# Utility to check the integrity of an IBAN bank account No.
1211
# based on https://www.daniweb.com/software-development/python/code/382069
1212
# /iban-number-check-refreshed
1213
# Dictionaries - Refer to ISO 7064 mod 97-10
1214
letter_dic = {
1215
    "A": 10,
1216
    "B": 11,
1217
    "C": 12,
1218
    "D": 13,
1219
    "E": 14,
1220
    "F": 15,
1221
    "G": 16,
1222
    "H": 17,
1223
    "I": 18,
1224
    "J": 19,
1225
    "K": 20,
1226
    "L": 21,
1227
    "M": 22,
1228
    "N": 23,
1229
    "O": 24,
1230
    "P": 25,
1231
    "Q": 26,
1232
    "R": 27,
1233
    "S": 28,
1234
    "T": 29,
1235
    "U": 30,
1236
    "V": 31,
1237
    "W": 32,
1238
    "X": 33,
1239
    "Y": 34,
1240
    "Z": 35,
1241
    "0": 0,
1242
    "1": 1,
1243
    "2": 2,
1244
    "3": 3,
1245
    "4": 4,
1246
    "5": 5,
1247
    "6": 6,
1248
    "7": 7,
1249
    "8": 8,
1250
    "9": 9
1251
}
1252
1253
# ISO 3166-1 alpha-2 country code
1254
country_dic = {
1255
    "AL": [28, "Albania"],
1256
    "AD": [24, "Andorra"],
1257
    "AT": [20, "Austria"],
1258
    "BE": [16, "Belgium"],
1259
    "BA": [20, "Bosnia"],
1260
    "BG": [22, "Bulgaria"],
1261
    "HR": [21, "Croatia"],
1262
    "CY": [28, "Cyprus"],
1263
    "CZ": [24, "Czech Republic"],
1264
    "DK": [18, "Denmark"],
1265
    "EE": [20, "Estonia"],
1266
    "FO": [18, "Faroe Islands"],
1267
    "FI": [18, "Finland"],
1268
    "FR": [27, "France"],
1269
    "DE": [22, "Germany"],
1270
    "GI": [23, "Gibraltar"],
1271
    "GR": [27, "Greece"],
1272
    "GL": [18, "Greenland"],
1273
    "HU": [28, "Hungary"],
1274
    "IS": [26, "Iceland"],
1275
    "IE": [22, "Ireland"],
1276
    "IL": [23, "Israel"],
1277
    "IT": [27, "Italy"],
1278
    "LV": [21, "Latvia"],
1279
    "LI": [21, "Liechtenstein"],
1280
    "LT": [20, "Lithuania"],
1281
    "LU": [20, "Luxembourg"],
1282
    "MK": [19, "Macedonia"],
1283
    "MT": [31, "Malta"],
1284
    "MU": [30, "Mauritius"],
1285
    "MC": [27, "Monaco"],
1286
    "ME": [22, "Montenegro"],
1287
    "NL": [18, "Netherlands"],
1288
    "NO": [15, "Northern Ireland"],
1289
    "PO": [28, "Poland"],
1290
    "PT": [25, "Portugal"],
1291
    "RO": [24, "Romania"],
1292
    "SM": [27, "San Marino"],
1293
    "SA": [24, "Saudi Arabia"],
1294
    "RS": [22, "Serbia"],
1295
    "SK": [24, "Slovakia"],
1296
    "SI": [19, "Slovenia"],
1297
    "ES": [24, "Spain"],
1298
    "SE": [24, "Sweden"],
1299
    "CH": [21, "Switzerland"],
1300
    "TR": [26, "Turkey"],
1301
    "TN": [24, "Tunisia"],
1302
    "GB": [22, "United Kingdom"]
1303
}
1304
1305
1306
class SortKeyValidator:
1307
    """ Check for out of range values.
1308
    """
1309
1310
    implements(IValidator)
1311
    name = "SortKeyValidator"
1312
1313
    def __call__(self, value, *args, **kwargs):
1314
        instance = kwargs['instance']
1315
        translate = getToolByName(instance, 'translation_service').translate
1316
        try:
1317
            value = float(value)
1318
        except:
1319
            msg = _("Validation failed: value must be float")
1320
            return to_utf8(translate(msg))
1321
1322
        if value < 0 or value > 1000:
1323
            msg = _("Validation failed: value must be between 0 and 1000")
1324
            return to_utf8(translate(msg))
1325
1326
        return True
1327
1328
1329
validation.register(SortKeyValidator())
1330
1331
1332
class InlineFieldValidator:
1333
    """ Inline Field Validator
1334
1335
    calls a field function for validation
1336
    """
1337
1338
    implements(IValidator)
1339
    name = "inline_field_validator"
1340
1341
    def __call__(self, value, *args, **kwargs):
1342
        field = kwargs['field']
1343
        request = kwargs['REQUEST']
1344
        instance = kwargs['instance']
1345
1346
        # extract the request values
1347
        data = request.get(field.getName())
1348
1349
        # check if the field contains a callable
1350
        validator = getattr(field, self.name, None)
1351
1352
        # validator is a callable
1353
        if callable(validator):
1354
            return validator(instance, request, field, data)
1355
1356
        # validator is a string, check if the instance has a method with this name
1357
        if type(validator) in types.StringTypes:
1358
            instance_validator = getattr(instance, validator, None)
1359
            if callable(instance_validator):
1360
                return instance_validator(request, field, data)
1361
1362
        return True
1363
1364
1365
validation.register(InlineFieldValidator())
1366
1367
1368
class NoWhiteSpaceValidator:
1369
    """ String, not containing space(s). """
1370
1371
    implements(IValidator)
1372
    name = "no_white_space_validator"
1373
1374
    def __call__(self, value, *args, **kwargs):
1375
        instance = kwargs['instance']
1376
        translate = getToolByName(instance, 'translation_service').translate
1377
1378
        if value and " " in value:
1379
            msg = _("Invalid value: Please enter a value without spaces.")
1380
            return to_utf8(translate(msg))
1381
1382
        return True
1383
1384
1385
validation.register(NoWhiteSpaceValidator())
1386
1387
1388
class ImportValidator(object):
1389
    """Checks if a dotted name can be imported or not
1390
    """
1391
    implements(IValidator)
1392
    name = "importvalidator"
1393
1394
    def __call__(self, mod, **kwargs):
1395
1396
        # some needed tools
1397
        instance = kwargs['instance']
1398
        translate = getToolByName(instance, 'translation_service').translate
1399
1400
        try:
1401
            # noinspection PyUnresolvedReferences
1402
            import importlib
1403
            importlib.import_module(mod)
1404
        except ImportError:
1405
            msg = _("Validation failed: Could not import module '%s'" % mod)
1406
            return to_utf8(translate(msg))
1407
1408
        return True
1409
1410
1411
validation.register(ImportValidator())
1412
1413
1414
class DefaultResultValidator(object):
1415
    """Validate AnalysisService's DefaultResult field value
1416
    """
1417
    implements(IValidator)
1418
    name = "service_defaultresult_validator"
1419
1420
    def __call__(self, value, **kwargs):
1421
        instance = kwargs['instance']
1422
        request = kwargs.get('REQUEST', {})
1423
        field_name = kwargs['field'].getName()
1424
        translate = getToolByName(instance, 'translation_service').translate
1425
1426
        default_result = request.get(field_name, None)
1427
        if default_result:
1428
            # Default result must be one of the available result options
1429
            options = request.get("ResultOptions", None)
1430
            if options:
1431
                values = map(lambda ro: ro.get("ResultValue"), options)
1432
                if default_result not in values:
1433
                    msg = _("Default result must be one of the following "
1434
                            "result options: {}").format(", ".join(values))
1435
                    return to_utf8(translate(msg))
1436
1437
            elif not request.get("StringResult"):
1438
                # Default result must be numeric
1439
                if not api.is_floatable(default_result):
1440
                    msg = _("Default result is not numeric")
1441
                    return to_utf8(translate(msg))
1442
1443
        return True
1444
1445
1446
validation.register(DefaultResultValidator())
1447
1448
1449
class ServiceConditionsValidator(object):
1450
    """Validate AnalysisService Conditions field
1451
    """
1452
    implements(IValidator)
1453
    name = "service_conditions_validator"
1454
1455
    def __call__(self, field_value, **kwargs):
1456
        instance = kwargs["instance"]
1457
        request = kwargs.get("REQUEST", {})
1458
        translate = getToolByName(instance, "translation_service").translate
1459
        field_name = kwargs["field"].getName()
1460
1461
        # This value in request prevents running once per subfield value.
1462
        # self.name returns the name of the validator. This allows other
1463
        # subfield validators to be called if defined (eg. in other add-ons)
1464
        key = "{}-{}-{}".format(self.name, instance.getId(), field_name)
1465
        if instance.REQUEST.get(key, False):
1466
            return True
1467
1468
        # Walk through all records set for this records field
1469
        field_name_value = "{}_value".format(field_name)
1470
        records = request.get(field_name_value, [])
1471
        for record in records:
1472
            # Validate the record
1473
            msg = self.validate_record(record)
1474
            if msg:
1475
                return to_utf8(translate(msg))
1476
1477
        instance.REQUEST[key] = True
1478
        return True
1479
1480
    def validate_record(self, record):
1481
        control_type = record.get("type")
1482
        choices = record.get("choices")
1483
        required = record.get("required") == "on"
1484
        default = record.get("default")
1485
1486
        if control_type == "select":
1487
            # choices is required, check if the value for subfield is ok
1488
            if not choices:
1489
                return _("Validation failed: value for Choices subfield is "
1490
                         "required when the control type of choice is "
1491
                         "'Select'")
1492
1493
            # Choices must follow the format  'choice 1|choice 2|choice 3'
1494
            choices_arr = filter(None, choices.split('|'))
1495
            if len(choices_arr) <= 1:
1496
                return _("Validation failed: Please use the character '|' "
1497
                         "to separate the available options in 'Choices' "
1498
                         "subfield")
1499
        elif control_type == "checkbox":
1500
            # required checkboxes need a default value to be submitted
1501
            if required and not default:
1502
                return _("Validation failed: Please set a default value "
1503
                         "when defining a required checkbox condition.")
1504
        else:
1505
            # choices should be left empty
1506
            if choices:
1507
                return _("Validation failed: value for Choices subfield is "
1508
                         "only required for when the control type of choice "
1509
                         "is 'Select'")
1510
1511
        # The type of the default value must match with the selected type
1512
        default_value = record.get("default")
1513
        if default_value:
1514
            if control_type == "number":
1515
                if not api.is_floatable(default_value):
1516
                    return _("Validation failed: '{}' is not numeric").format(
1517
                        default_value)
1518
1519
1520
validation.register(ServiceConditionsValidator())
1521
>>>>>>> 2.x
1522