Passed
Pull Request — 2.x (#1987)
by
unknown
05:15
created

InterimFieldsValidator.validate_choices()   C

Complexity

Conditions 9

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 34
rs 6.6666
c 0
b 0
f 0
cc 9
nop 2
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 View Code Duplication
class IdentifierTypeAttributesValidator:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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 View Code Duplication
class IdentifierValidator:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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, 'senaite_catalog_setup')
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, 'senaite_catalog_setup')
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 View Code Duplication
            if 'title' in field and \
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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 View Code Duplication
            if 'keyword' in field and \
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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, result type should remain empty
454
            result_type = interim.get("result_type")
455
            if not result_type:
456
                return
457
            return _t(_("Control type is not supported for empty choices"))
458
459
        # Choices are expressed like "value0:text0|value1:text1|..|valuen:textn"
460
        choices = choices.split("|") or []
461
        try:
462
            choices = dict(map(lambda ch: ch.strip().split(":"), choices))
463
        except ValueError:
464
            return _t(_(
465
                "No valid format in choices field. Supported format is: "
466
                "<value-0>:<text>|<value-1>:<text>|<value-n>:<text>"))
467
468
        # Empty keys (that match with the result value) are not allowed
469
        keys = map(lambda k: k.strip(), choices.keys())
470
        empties = filter(None, keys)
471
        if len(empties) != len(keys):
472
            return _t(_("Empty keys are not supported"))
473
474
        # No duplicate keys allowed
475
        unique_keys = list(set(keys))
476
        if len(unique_keys) != len(keys):
477
            return _t(_("Duplicate keys in choices field"))
478
479
        # We need at least 2 choices
480
        if len(keys) < 2:
481
            return _t(_("At least, two options for choices field are required"))
482
483
484
validation.register(InterimFieldsValidator())
485
486
487
class FormulaValidator:
488
    """ Validate keywords in calculation formula entry
489
    """
490
    implements(IValidator)
491
    name = "formulavalidator"
492
493
    def __call__(self, value, *args, **kwargs):
494
        if not value:
495
            return True
496
        instance = kwargs["instance"]
497
        request = api.get_request()
498
        form = request.form
499
        interim_fields = form.get("InterimFields", [])
500
        translate = getToolByName(instance, "translation_service").translate
501
        catalog = api.get_tool(SETUP_CATALOG)
502
        interim_keywords = filter(None, map(
503
            lambda i: i.get("keyword"), interim_fields))
504
        keywords = re.compile(r"\[([^\.^\]]+)\]").findall(value)
505
506
        for keyword in keywords:
507
            # Check if the service keyword exists and is active.
508
            dep_service = catalog(getKeyword=keyword, is_active=True)
509
            if not dep_service and keyword not in interim_keywords:
510
                msg = _(
511
                    "Validation failed: Keyword '${keyword}' is invalid",
512
                    mapping={
513
                        'keyword': safe_unicode(keyword)
514
                    })
515
                return to_utf8(translate(msg))
516
517
        # Allow to use Wildcards, LDL and UDL values in calculations
518
        allowedwds = ["LDL", "UDL", "BELOWLDL", "ABOVEUDL"]
519
        keysandwildcards = re.compile(r"\[([^\]]+)\]").findall(value)
520
        keysandwildcards = [k for k in keysandwildcards if "." in k]
521
        keysandwildcards = [k.split(".", 1) for k in keysandwildcards]
522
        errwilds = [k[1] for k in keysandwildcards if k[0] not in keywords]
523
        if len(errwilds) > 0:
524
            msg = _(
525
                "Wildcards for interims are not allowed: ${wildcards}",
526
                mapping={
527
                    "wildcards": safe_unicode(", ".join(errwilds))
528
                })
529
            return to_utf8(translate(msg))
530
531
        wildcards = [k[1] for k in keysandwildcards if k[0] in keywords]
532
        wildcards = [wd for wd in wildcards if wd not in allowedwds]
533
        if len(wildcards) > 0:
534
            msg = _(
535
                "Invalid wildcards found: ${wildcards}",
536
                mapping={
537
                    "wildcards": safe_unicode(", ".join(wildcards))
538
                })
539
            return to_utf8(translate(msg))
540
541
        return True
542
543
544
validation.register(FormulaValidator())
545
546
547
class CoordinateValidator:
548
    """ Validate latitude or longitude field values
549
    """
550
    implements(IValidator)
551
    name = "coordinatevalidator"
552
553
    def __call__(self, value, **kwargs):
554
        if not value:
555
            return True
556
557
        instance = kwargs['instance']
558
        fieldname = kwargs['field'].getName()
559
        request = instance.REQUEST
560
561
        form = request.form
562
        form_value = form.get(fieldname)
563
564
        translate = getToolByName(instance, 'translation_service').translate
565
566
        try:
567
            degrees = int(form_value['degrees'])
568
        except ValueError:
569
            return to_utf8(
570
                translate(_("Validation failed: degrees must be numeric")))
571
572
        try:
573
            minutes = int(form_value['minutes'])
574
        except ValueError:
575
            return to_utf8(
576
                translate(_("Validation failed: minutes must be numeric")))
577
578
        try:
579
            seconds = int(form_value['seconds'])
580
        except ValueError:
581
            return to_utf8(
582
                translate(_("Validation failed: seconds must be numeric")))
583
584
        if not 0 <= minutes <= 59:
585
            return to_utf8(
586
                translate(_("Validation failed: minutes must be 0 - 59")))
587
588
        if not 0 <= seconds <= 59:
589
            return to_utf8(
590
                translate(_("Validation failed: seconds must be 0 - 59")))
591
592
        bearing = form_value['bearing']
593
594 View Code Duplication
        if fieldname == 'Latitude':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
595
            if not 0 <= degrees <= 90:
596
                return to_utf8(
597
                    translate(_("Validation failed: degrees must be 0 - 90")))
598
            if degrees == 90:
599
                if minutes != 0:
600
                    return to_utf8(
601
                        translate(
602
                            _("Validation failed: degrees is 90; "
603
                              "minutes must be zero")))
604
                if seconds != 0:
605
                    return to_utf8(
606
                        translate(
607
                            _("Validation failed: degrees is 90; "
608
                              "seconds must be zero")))
609
            if bearing.lower() not in 'sn':
610
                return to_utf8(
611
                    translate(_("Validation failed: Bearing must be N/S")))
612
613 View Code Duplication
        if fieldname == 'Longitude':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
614
            if not 0 <= degrees <= 180:
615
                return to_utf8(
616
                    translate(_("Validation failed: degrees must be 0 - 180")))
617
            if degrees == 180:
618
                if minutes != 0:
619
                    return to_utf8(
620
                        translate(
621
                            _("Validation failed: degrees is 180; "
622
                              "minutes must be zero")))
623
                if seconds != 0:
624
                    return to_utf8(
625
                        translate(
626
                            _("Validation failed: degrees is 180; "
627
                              "seconds must be zero")))
628
            if bearing.lower() not in 'ew':
629
                return to_utf8(
630
                    translate(_("Validation failed: Bearing must be E/W")))
631
632
        return True
633
634
635
validation.register(CoordinateValidator())
636
637
638
class ResultOptionsValueValidator(object):
639
    """Validator for the subfield "ResultValue" of ResultOptions field
640
    """
641
642
    implements(IValidator)
643
    name = "result_options_value_validator"
644
645
    def __call__(self, value, *args, **kwargs):
646
        # Result Value must be floatable
647
        if not api.is_floatable(value):
648
            return _t(_("Result Value must be a number"))
649
650
        # Get all records
651
        instance = kwargs['instance']
652
        field_name = kwargs['field'].getName()
653
        request = instance.REQUEST
654
        records = request.form.get(field_name)
655
656
        # Result values must be unique
657
        value = api.to_float(value)
658
        values = map(lambda ro: ro.get("ResultValue"), records)
659
        values = filter(api.is_floatable, values)
660
        values = map(api.to_float, values)
661
        duplicates = filter(lambda val: val == value, values)
662
        if len(duplicates) > 1:
663
            return _t(_("Result Value must be unique"))
664
665
        return True
666
667
668
validation.register(ResultOptionsValueValidator())
669
670
671
class ResultOptionsTextValidator(object):
672
    """Validator for the subfield "ResultText" of ResultsOption field
673
    """
674
675
    implements(IValidator)
676
    name = "result_options_text_validator"
677
678
    def __call__(self, value, *args, **kwargs):
679
        # Result Text is required
680
        if not value or not value.strip():
681
            return _t(_("Display Value is required"))
682
683
        # Get all records
684
        instance = kwargs['instance']
685
        field_name = kwargs['field'].getName()
686
        request = instance.REQUEST
687
        records = request.form.get(field_name)
688
689
        # Result Text must be unique
690
        original_texts = map(lambda ro: ro.get("ResultText"), records)
691
        duplicates = filter(lambda text: text == value, original_texts)
692
        if len(duplicates) > 1:
693
            return _t(_("Display Value must be unique"))
694
695
        return True
696
697
698
validation.register(ResultOptionsTextValidator())
699
700
701
class RestrictedCategoriesValidator:
702
    """ Verifies that client Restricted categories include all categories
703
    required by service dependencies. """
704
705
    implements(IValidator)
706
    name = "restrictedcategoriesvalidator"
707
708
    def __call__(self, value, *args, **kwargs):
709
        instance = kwargs['instance']
710
        # fieldname = kwargs['field'].getName()
711
        # request = kwargs.get('REQUEST', {})
712
        # form = request.get('form', {})
713
714
        translate = getToolByName(instance, 'translation_service').translate
715
        bsc = getToolByName(instance, 'senaite_catalog_setup')
716
        # uc = getToolByName(instance, 'uid_catalog')
717
718
        failures = []
719
720
        for category in value:
721
            if not category:
722
                continue
723
            services = bsc(portal_type="AnalysisService", category_uid=category)
724
            for service in services:
725
                service = service.getObject()
726
                calc = service.getCalculation()
727
                deps = calc and calc.getDependentServices() or []
728
                for dep in deps:
729
                    if dep.getCategoryUID() not in value:
730
                        title = dep.getCategoryTitle()
731
                        if title not in failures:
732
                            failures.append(title)
733
        if failures:
734
            msg = _(
735
                "Validation failed: The selection requires the following "
736
                "categories to be selected: ${categories}",
737
                mapping={
738
                    'categories': safe_unicode(','.join(failures))
739
                })
740
            return to_utf8(translate(msg))
741
742
        return True
743
744
745
validation.register(RestrictedCategoriesValidator())
746
747
748
class PrePreservationValidator:
749
    """ Validate PrePreserved Containers.
750
        User must select a Preservation.
751
    """
752
    implements(IValidator)
753
    name = "container_prepreservation_validator"
754
755
    def __call__(self, value, *args, **kwargs):
756
        # If not prepreserved, no validation required.
757
        if not value:
758
            return True
759
760
        instance = kwargs['instance']
761
        # fieldname = kwargs['field'].getName()
762
        request = kwargs.get('REQUEST', {})
763
        form = request.form
764
        preservation = form.get('Preservation')
765
766
        if type(preservation) in (list, tuple):
767
            preservation = preservation[0]
768
769
        if preservation:
770
            return True
771
772
        translate = getToolByName(instance, 'translation_service').translate
773
        # bsc = getToolByName(instance, 'senaite_catalog_setup')
774
775
        if not preservation:
776
            msg = _("Validation failed: PrePreserved containers "
777
                    "must have a preservation selected.")
778
            return to_utf8(translate(msg))
779
780
781
validation.register(PrePreservationValidator())
782
783
784
class StandardIDValidator:
785
    """Matches against regular expression:
786
       [^A-Za-z\w\d\-\_]
787
    """
788
789
    implements(IValidator)
790
    name = "standard_id_validator"
791
792
    def __call__(self, value, *args, **kwargs):
793
794
        regex = r"[^A-Za-z\w\d\-\_]"
795
796
        instance = kwargs['instance']
797
        # fieldname = kwargs['field'].getName()
798
        # request = kwargs.get('REQUEST', {})
799
        # form = request.get('form', {})
800
801
        translate = getToolByName(instance, 'translation_service').translate
802
803
        # check the value against all AnalysisService keywords
804
        if re.findall(regex, value):
805
            msg = _("Validation failed: keyword contains invalid "
806
                    "characters")
807
            return to_utf8(translate(msg))
808
809
        return True
810
811
812
validation.register(StandardIDValidator())
813
814
815
def get_record_value(request, uid, keyword, default=None):
816
    """Returns the value for the keyword and uid from the request"""
817
    value = request.get(keyword)
818
    if not value:
819
        return default
820
    if not isinstance(value, list):
821
        return default
822
    return value[0].get(uid, default) or default
823
824
825
class AnalysisSpecificationsValidator:
826
    """Min value must be below max value
827
       Warn min value must be below min value or empty
828
       Warn max value must above max value or empty
829
       Percentage value must be between 0 and 100
830
       Values must be numbers
831
    """
832
833
    implements(IValidator)
834
    name = "analysisspecs_validator"
835
836
    def __call__(self, value, *args, **kwargs):
837
        instance = kwargs["instance"]
838
        request = kwargs.get("REQUEST", {})
839
        fieldname = kwargs["field"].getName()
840
841
        # This value in request prevents running once per subfield value.
842
        # self.name returns the name of the validator. This allows other
843
        # subfield validators to be called if defined (eg. in other add-ons)
844
        key = "{}-{}-{}".format(self.name, instance.getId(), fieldname)
845
        if instance.REQUEST.get(key, False):
846
            return True
847
848
        # Walk through all AS UIDs and validate each parameter for that AS
849
        service_uids = request.get("uids", [])
850
        for uid in service_uids:
851
            err_msg = self.validate_service(request, uid)
852
            if not err_msg:
853
                continue
854
855
            # Validation failed
856
            service = api.get_object_by_uid(uid)
857
            title = api.get_title(service)
858
859
            err_msg = "{}: {}".format(title, _(err_msg))
860
            translate = api.get_tool("translation_service").translate
861
            instance.REQUEST[key] = to_utf8(translate(safe_unicode(err_msg)))
862
            return instance.REQUEST[key]
863
864
        instance.REQUEST[key] = True
865
        return True
866
867
    def validate_service(self, request, uid):
868
        """Validates the specs values from request for the service uid. Returns
869
        a non-translated message if the validation failed.
870
        """
871
        spec_min = get_record_value(request, uid, "min")
872
        spec_max = get_record_value(request, uid, "max")
873
874
        warn_min = get_record_value(request, uid, "warn_min")
875
        warn_max = get_record_value(request, uid, "warn_max")
876
877
        if not spec_min and not spec_max:
878
            # Neither min nor max values have been set, dismiss
879
            return None
880
881
        # Allow to have empty min/max borders, e.g. only max being set
882
        if spec_min and not api.is_floatable(spec_min):
883
            return _("'Min' value must be numeric")
884
        if spec_max and not api.is_floatable(spec_max):
885
            return _("'Max' value must be numeric")
886
887
        # Check if min is smaller than max range
888
        if spec_min and spec_max:
889
            if api.to_float(spec_min) > api.to_float(spec_max):
890
                return _("'Max' value must be above 'Min' value")
891
892
        # Handle warn min
893
        if warn_min and not api.is_floatable(warn_min):
894
            return _("'Warn Min' value must be numeric or empty")
895
        if warn_min and spec_min:
896
            if api.to_float(warn_min) > api.to_float(spec_min):
897
                return _("'Warn Min' value must be below 'Min' value")
898
899
        # Handle warn max
900
        if warn_max and not api.is_floatable(warn_max):
901
            return _("'Warn Max' value must be numeric or empty")
902
        if warn_max and spec_max:
903
            if api.to_float(warn_max) < api.to_float(spec_max):
904
                return _("'Warn Max' value must be above 'Max' value")
905
906
        return None
907
908
909
validation.register(AnalysisSpecificationsValidator())
910
911
912
class UncertaintiesValidator:
913
    """Uncertainties may be specified as numeric values or percentages.
914
    Min value must be below max value.
915
    Uncertainty must not be < 0.
916
    """
917
918
    implements(IValidator)
919
    name = "uncertainties_validator"
920
921
    def __call__(self, subf_value, *args, **kwargs):
922
923
        instance = kwargs['instance']
924
        request = kwargs.get('REQUEST', {})
925
        fieldname = kwargs['field'].getName()
926
        translate = getToolByName(instance, 'translation_service').translate
927
928
        # We run through the validator once per form submit, and check all
929
        # values
930
        # this value in request prevents running once per subfield value.
931
        key = instance.id + fieldname
932
        if instance.REQUEST.get(key, False):
933
            return True
934
935
        for i, value in enumerate(request[fieldname]):
936
937
            # Values must be numbers
938
            try:
939
                minv = float(value['intercept_min'])
940
            except ValueError:
941
                instance.REQUEST[key] = to_utf8(
942
                    translate(
943
                        _("Validation failed: Min values must be numeric")))
944
                return instance.REQUEST[key]
945
            try:
946
                maxv = float(value['intercept_max'])
947
            except ValueError:
948
                instance.REQUEST[key] = to_utf8(
949
                    translate(
950
                        _("Validation failed: Max values must be numeric")))
951
                return instance.REQUEST[key]
952
953
            # values may be percentages; the rest of the numeric validation must
954
            # still pass once the '%' is stripped off.
955
            err = value['errorvalue']
956
            perc = False
957
            if err.endswith('%'):
958
                perc = True
959
                err = err[:-1]
960
            try:
961
                err = float(err)
962
            except ValueError:
963
                instance.REQUEST[key] = to_utf8(
964
                    translate(
965
                        _("Validation failed: Error values must be numeric")))
966
                return instance.REQUEST[key]
967
968
            if perc and (err < 0 or err > 100):
969
                # Error percentage must be between 0 and 100
970
                instance.REQUEST[key] = to_utf8(
971
                    translate(
972
                        _("Validation failed: Error percentage must be between 0 "
973
                          "and 100")))
974
                return instance.REQUEST[key]
975
976
            # Min value must be < max
977
            if minv > maxv:
978
                instance.REQUEST[key] = to_utf8(
979
                    translate(
980
                        _("Validation failed: Max values must be greater than Min "
981
                          "values")))
982
                return instance.REQUEST[key]
983
984
            # Error values must be >-1
985
            if err < 0:
986
                instance.REQUEST[key] = to_utf8(
987
                    translate(
988
                        _("Validation failed: Error value must be 0 or greater"
989
                          )))
990
                return instance.REQUEST[key]
991
992
        instance.REQUEST[key] = True
993
        return True
994
995
996
validation.register(UncertaintiesValidator())
997
998
999
class DurationValidator:
1000
    """Simple stuff - just checking for integer values.
1001
    """
1002
1003
    implements(IValidator)
1004
    name = "duration_validator"
1005
1006
    def __call__(self, value, *args, **kwargs):
1007
1008
        instance = kwargs['instance']
1009
        request = kwargs.get('REQUEST') or {}
1010
        fieldname = kwargs['field'].getName()
1011
        translate = getToolByName(instance, 'translation_service').translate
1012
1013
        value = request.get(fieldname) or None
1014
        if value:
1015
            for v in value.values():
1016
                try:
1017
                    int(v)
1018
                except:
1019
                    return to_utf8(
1020
                        translate(_("Validation failed: Values must be numbers")))
1021
        return True
1022
1023
1024
validation.register(DurationValidator())
1025
1026
1027
class ReferenceValuesValidator:
1028
    """Min value must be below max value
1029
       Percentage value must be between 0 and 100
1030
       Values must be numbers
1031
       Expected values must be between min and max values
1032
    """
1033
1034
    implements(IValidator)
1035
    name = "referencevalues_validator"
1036
1037
    def __call__(self, value, *args, **kwargs):
1038
        request = kwargs.get('REQUEST', {})
1039
        # Retrieve all AS uids
1040
        services = request.get('service', [{}])[0]
1041
        for uid, service_name in services.items():
1042
            err_msg = self.validate_service(request, uid)
1043
            if not err_msg:
1044
                continue
1045
1046
            # Validation failed
1047
            err_msg = "{}: {}".format(_("Validation for '{}' failed"),
1048
                                      _(err_msg))
1049
            err_msg = err_msg.format(service_name)
1050
            translate = api.get_tool('translation_service').translate
1051
            return to_utf8(translate(safe_unicode(err_msg)))
1052
1053
        return True
1054
1055
    def validate_service(self, request, uid):
1056
        """Validates the specs values from request for the service uid. Returns
1057
        a non-translated message if the validation failed."""
1058
1059
        result = get_record_value(request, uid, 'result')
1060
        if not result:
1061
            # No result set for this service, dismiss
1062
            return None
1063
1064
        if not api.is_floatable(result):
1065
            return "Expected result value must be numeric"
1066
1067
        spec_min = get_record_value(request, uid, "min", result)
1068
        spec_max = get_record_value(request, uid, "max", result)
1069
        error = get_record_value(request, uid, "error", "0")
1070
        if not api.is_floatable(spec_min):
1071
            return "'Min' value must be numeric"
1072
        if not api.is_floatable(spec_max):
1073
            return "'Max' value must be numeric"
1074
        if api.to_float(spec_min) > api.to_float(result):
1075
            return "'Min' value must be below the expected result"
1076
        if api.to_float(spec_max) < api.to_float(result):
1077
            return "'Max' value must be above the expected result"
1078
        if not api.is_floatable(error) or 0.0 < api.to_float(error) > 100:
1079
            return "% Error must be between 0 and 100"
1080
        return None
1081
1082
validation.register(ReferenceValuesValidator())
1083
1084
1085 View Code Duplication
class PercentValidator:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1086
    """ Floatable, >=0, <=100. """
1087
1088
    implements(IValidator)
1089
    name = "percentvalidator"
1090
1091
    def __call__(self, value, *args, **kwargs):
1092
        instance = kwargs['instance']
1093
        # fieldname = kwargs['field'].getName()
1094
        # request = kwargs.get('REQUEST', {})
1095
        # form = request.get('form', {})
1096
1097
        translate = getToolByName(instance, 'translation_service').translate
1098
1099
        try:
1100
            value = float(value)
1101
        except:
1102
            msg = _("Validation failed: percent values must be numbers")
1103
            return to_utf8(translate(msg))
1104
1105
        if value < 0 or value > 100:
1106
            msg = _(
1107
                "Validation failed: percent values must be between 0 and 100")
1108
            return to_utf8(translate(msg))
1109
1110
        return True
1111
1112
1113
validation.register(PercentValidator())
1114
1115
1116
def _toIntList(numstr, acceptX=0):
1117
    """
1118
    Convert ans string to a list removing all invalid characters.
1119
    Receive: a string as a number
1120
    """
1121
    res = []
1122
    # Converting and removing invalid characters
1123
    for i in numstr:
1124
        if i in string.digits and i not in string.letters:
1125
            res.append(int(i))
1126
1127
    # Converting control number into ISBN
1128
    if acceptX and (numstr[-1] in 'Xx'):
1129
        res.append(10)
1130
    return res
1131
1132
1133
def _sumLists(a, b):
1134
    """
1135
    Algorithm to check validity of NBI and NIF.
1136
    Receives string with a umber to validate.
1137
    """
1138
    val = 0
1139
    for i in map(lambda a, b: a * b, a, b):
1140
        val += i
1141
    return val
1142
1143
1144
class NIBvalidator:
1145
    """
1146
    Validates if the introduced NIB is correct.
1147
    """
1148
1149
    implements(IValidator)
1150
    name = "NIBvalidator"
1151
1152
    def __call__(self, value, *args, **kwargs):
1153
        """
1154
        Check the NIB number
1155
        value:: string with NIB.
1156
        """
1157
        instance = kwargs['instance']
1158
        translate = getToolByName(instance, 'translation_service').translate
1159
        LEN_NIB = 21
1160
        table = (73, 17, 89, 38, 62, 45, 53, 15, 50, 5, 49, 34, 81, 76, 27, 90,
1161
                 9, 30, 3)
1162
1163
        # convert to entire numbers list
1164
        nib = _toIntList(value)
1165
1166
        # checking the length of the number
1167
        if len(nib) != LEN_NIB:
1168
            msg = _('Incorrect NIB number: %s' % value)
1169
            return to_utf8(translate(msg))
1170
        # last numbers algorithm validator
1171
        return nib[-2] * 10 + nib[-1] == 98 - _sumLists(table, nib[:-2]) % 97
1172
1173
1174
validation.register(NIBvalidator())
1175
1176
1177
class IBANvalidator:
1178
    """
1179
    Validates if the introduced NIB is correct.
1180
    """
1181
1182
    implements(IValidator)
1183
    name = "IBANvalidator"
1184
1185
    def __call__(self, value, *args, **kwargs):
1186
        instance = kwargs['instance']
1187
        translate = getToolByName(instance, 'translation_service').translate
1188
1189
        # remove spaces from formatted
1190
        IBAN = ''.join(c for c in value if c.isalnum())
1191
1192
        IBAN = IBAN[4:] + IBAN[:4]
1193
        country = IBAN[-4:-2]
1194
1195
        if country not in country_dic:
1196
            msg = _('Unknown IBAN country %s' % country)
1197
            return to_utf8(translate(msg))
1198
1199
        length_c, name_c = country_dic[country]
1200
1201
        if len(IBAN) != length_c:
1202
            diff = len(IBAN) - length_c
1203
            msg = _('Wrong IBAN length by %s: %s' %
1204
                    (('short by %i' % -diff)
1205
                     if diff < 0 else ('too long by %i' % diff), value))
1206
            return to_utf8(translate(msg))
1207
        # Validating procedure
1208
        elif int("".join(str(letter_dic[x]) for x in IBAN)) % 97 != 1:
1209
            msg = _('Incorrect IBAN number: %s' % value)
1210
            return to_utf8(translate(msg))
1211
1212
        else:
1213
            # Accepted:
1214
            return True
1215
1216
1217
validation.register(IBANvalidator())
1218
1219
# Utility to check the integrity of an IBAN bank account No.
1220
# based on https://www.daniweb.com/software-development/python/code/382069
1221
# /iban-number-check-refreshed
1222
# Dictionaries - Refer to ISO 7064 mod 97-10
1223
letter_dic = {
1224
    "A": 10,
1225
    "B": 11,
1226
    "C": 12,
1227
    "D": 13,
1228
    "E": 14,
1229
    "F": 15,
1230
    "G": 16,
1231
    "H": 17,
1232
    "I": 18,
1233
    "J": 19,
1234
    "K": 20,
1235
    "L": 21,
1236
    "M": 22,
1237
    "N": 23,
1238
    "O": 24,
1239
    "P": 25,
1240
    "Q": 26,
1241
    "R": 27,
1242
    "S": 28,
1243
    "T": 29,
1244
    "U": 30,
1245
    "V": 31,
1246
    "W": 32,
1247
    "X": 33,
1248
    "Y": 34,
1249
    "Z": 35,
1250
    "0": 0,
1251
    "1": 1,
1252
    "2": 2,
1253
    "3": 3,
1254
    "4": 4,
1255
    "5": 5,
1256
    "6": 6,
1257
    "7": 7,
1258
    "8": 8,
1259
    "9": 9
1260
}
1261
1262
# ISO 3166-1 alpha-2 country code
1263
country_dic = {
1264
    "AL": [28, "Albania"],
1265
    "AD": [24, "Andorra"],
1266
    "AT": [20, "Austria"],
1267
    "BE": [16, "Belgium"],
1268
    "BA": [20, "Bosnia"],
1269
    "BG": [22, "Bulgaria"],
1270
    "HR": [21, "Croatia"],
1271
    "CY": [28, "Cyprus"],
1272
    "CZ": [24, "Czech Republic"],
1273
    "DK": [18, "Denmark"],
1274
    "EE": [20, "Estonia"],
1275
    "FO": [18, "Faroe Islands"],
1276
    "FI": [18, "Finland"],
1277
    "FR": [27, "France"],
1278
    "DE": [22, "Germany"],
1279
    "GI": [23, "Gibraltar"],
1280
    "GR": [27, "Greece"],
1281
    "GL": [18, "Greenland"],
1282
    "HU": [28, "Hungary"],
1283
    "IS": [26, "Iceland"],
1284
    "IE": [22, "Ireland"],
1285
    "IL": [23, "Israel"],
1286
    "IT": [27, "Italy"],
1287
    "LV": [21, "Latvia"],
1288
    "LI": [21, "Liechtenstein"],
1289
    "LT": [20, "Lithuania"],
1290
    "LU": [20, "Luxembourg"],
1291
    "MK": [19, "Macedonia"],
1292
    "MT": [31, "Malta"],
1293
    "MU": [30, "Mauritius"],
1294
    "MC": [27, "Monaco"],
1295
    "ME": [22, "Montenegro"],
1296
    "NL": [18, "Netherlands"],
1297
    "NO": [15, "Northern Ireland"],
1298
    "PO": [28, "Poland"],
1299
    "PT": [25, "Portugal"],
1300
    "RO": [24, "Romania"],
1301
    "SM": [27, "San Marino"],
1302
    "SA": [24, "Saudi Arabia"],
1303
    "RS": [22, "Serbia"],
1304
    "SK": [24, "Slovakia"],
1305
    "SI": [19, "Slovenia"],
1306
    "ES": [24, "Spain"],
1307
    "SE": [24, "Sweden"],
1308
    "CH": [21, "Switzerland"],
1309
    "TR": [26, "Turkey"],
1310
    "TN": [24, "Tunisia"],
1311
    "GB": [22, "United Kingdom"]
1312
}
1313
1314
1315 View Code Duplication
class SortKeyValidator:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1316
    """ Check for out of range values.
1317
    """
1318
1319
    implements(IValidator)
1320
    name = "SortKeyValidator"
1321
1322
    def __call__(self, value, *args, **kwargs):
1323
        instance = kwargs['instance']
1324
        translate = getToolByName(instance, 'translation_service').translate
1325
        try:
1326
            value = float(value)
1327
        except:
1328
            msg = _("Validation failed: value must be float")
1329
            return to_utf8(translate(msg))
1330
1331
        if value < 0 or value > 1000:
1332
            msg = _("Validation failed: value must be between 0 and 1000")
1333
            return to_utf8(translate(msg))
1334
1335
        return True
1336
1337
1338
validation.register(SortKeyValidator())
1339
1340
1341
class InlineFieldValidator:
1342
    """ Inline Field Validator
1343
1344
    calls a field function for validation
1345
    """
1346
1347
    implements(IValidator)
1348
    name = "inline_field_validator"
1349
1350
    def __call__(self, value, *args, **kwargs):
1351
        field = kwargs['field']
1352
        request = kwargs['REQUEST']
1353
        instance = kwargs['instance']
1354
1355
        # extract the request values
1356
        data = request.get(field.getName())
1357
1358
        # check if the field contains a callable
1359
        validator = getattr(field, self.name, None)
1360
1361
        # validator is a callable
1362
        if callable(validator):
1363
            return validator(instance, request, field, data)
1364
1365
        # validator is a string, check if the instance has a method with this name
1366
        if type(validator) in types.StringTypes:
1367
            instance_validator = getattr(instance, validator, None)
1368
            if callable(instance_validator):
1369
                return instance_validator(request, field, data)
1370
1371
        return True
1372
1373
1374
validation.register(InlineFieldValidator())
1375
1376
1377
class NoWhiteSpaceValidator:
1378
    """ String, not containing space(s). """
1379
1380
    implements(IValidator)
1381
    name = "no_white_space_validator"
1382
1383
    def __call__(self, value, *args, **kwargs):
1384
        instance = kwargs['instance']
1385
        translate = getToolByName(instance, 'translation_service').translate
1386
1387
        if value and " " in value:
1388
            msg = _("Invalid value: Please enter a value without spaces.")
1389
            return to_utf8(translate(msg))
1390
1391
        return True
1392
1393
1394
validation.register(NoWhiteSpaceValidator())
1395
1396
1397
class ImportValidator(object):
1398
    """Checks if a dotted name can be imported or not
1399
    """
1400
    implements(IValidator)
1401
    name = "importvalidator"
1402
1403
    def __call__(self, mod, **kwargs):
1404
1405
        # some needed tools
1406
        instance = kwargs['instance']
1407
        translate = getToolByName(instance, 'translation_service').translate
1408
1409
        try:
1410
            # noinspection PyUnresolvedReferences
1411
            import importlib
1412
            importlib.import_module(mod)
1413
        except ImportError:
1414
            msg = _("Validation failed: Could not import module '%s'" % mod)
1415
            return to_utf8(translate(msg))
1416
1417
        return True
1418
1419
1420
validation.register(ImportValidator())
1421
1422
1423
class DefaultResultValidator(object):
1424
    """Validate AnalysisService's DefaultResult field value
1425
    """
1426
    implements(IValidator)
1427
    name = "service_defaultresult_validator"
1428
1429
    def __call__(self, value, **kwargs):
1430
        instance = kwargs['instance']
1431
        request = kwargs.get('REQUEST', {})
1432
        field_name = kwargs['field'].getName()
1433
        translate = getToolByName(instance, 'translation_service').translate
1434
1435
        default_result = request.get(field_name, None)
1436
        if default_result:
1437
            # Default result must be one of the available result options
1438
            options = request.get("ResultOptions", None)
1439
            if options:
1440
                values = map(lambda ro: ro.get("ResultValue"), options)
1441
                if default_result not in values:
1442
                    msg = _("Default result must be one of the following "
1443
                            "result options: {}").format(", ".join(values))
1444
                    return to_utf8(translate(msg))
1445
1446
            elif not request.get("StringResult"):
1447
                # Default result must be numeric
1448
                if not api.is_floatable(default_result):
1449
                    msg = _("Default result is not numeric")
1450
                    return to_utf8(translate(msg))
1451
1452
        return True
1453
1454
1455
validation.register(DefaultResultValidator())
1456
1457
1458
class ServiceConditionsValidator(object):
1459
    """Validate AnalysisService Conditions field
1460
    """
1461
    implements(IValidator)
1462
    name = "service_conditions_validator"
1463
1464
    def __call__(self, field_value, **kwargs):
1465
        instance = kwargs["instance"]
1466
        request = kwargs.get("REQUEST", {})
1467
        translate = getToolByName(instance, "translation_service").translate
1468
        field_name = kwargs["field"].getName()
1469
1470
        # This value in request prevents running once per subfield value.
1471
        # self.name returns the name of the validator. This allows other
1472
        # subfield validators to be called if defined (eg. in other add-ons)
1473
        key = "{}-{}-{}".format(self.name, instance.getId(), field_name)
1474
        if instance.REQUEST.get(key, False):
1475
            return True
1476
1477
        # Walk through all records set for this records field
1478
        field_name_value = "{}_value".format(field_name)
1479
        records = request.get(field_name_value, [])
1480
        for record in records:
1481
            # Validate the record
1482
            msg = self.validate_record(record)
1483
            if msg:
1484
                return to_utf8(translate(msg))
1485
1486
        instance.REQUEST[key] = True
1487
        return True
1488
1489
    def validate_record(self, record):
1490
        control_type = record.get("type")
1491
        choices = record.get("choices")
1492
        required = record.get("required") == "on"
1493
        default = record.get("default")
1494
1495
        if control_type == "select":
1496
            # choices is required, check if the value for subfield is ok
1497
            if not choices:
1498
                return _("Validation failed: value for Choices subfield is "
1499
                         "required when the control type of choice is "
1500
                         "'Select'")
1501
1502
            # Choices must follow the format  'choice 1|choice 2|choice 3'
1503
            choices_arr = filter(None, choices.split('|'))
1504
            if len(choices_arr) <= 1:
1505
                return _("Validation failed: Please use the character '|' "
1506
                         "to separate the available options in 'Choices' "
1507
                         "subfield")
1508
        elif control_type == "checkbox":
1509
            # required checkboxes need a default value to be submitted
1510
            if required and not default:
1511
                return _("Validation failed: Please set a default value "
1512
                         "when defining a required checkbox condition.")
1513
        else:
1514
            # choices should be left empty
1515
            if choices:
1516
                return _("Validation failed: value for Choices subfield is "
1517
                         "only required for when the control type of choice "
1518
                         "is 'Select'")
1519
1520
        # The type of the default value must match with the selected type
1521
        default_value = record.get("default")
1522
        if default_value:
1523
            if control_type == "number":
1524
                if not api.is_floatable(default_value):
1525
                    return _("Validation failed: '{}' is not numeric").format(
1526
                        default_value)
1527
1528
1529
validation.register(ServiceConditionsValidator())
1530