Passed
Push — 2.x ( 777ce6...78af08 )
by Jordi
07:13
created

AnalysisSpecificationsValidator.validate_service()   F

Complexity

Conditions 20

Size

Total Lines 40
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 25
dl 0
loc 40
rs 0
c 0
b 0
f 0
cc 20
nop 3

How to fix   Complexity   

Complexity

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