UniqueFieldValidator.query_parent_objects()   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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