Passed
Push — 2.x ( 3f40bd...d670c9 )
by Ramon
05:54 queued 01:27
created

RecordField.isRequired()   A

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nop 2
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2021 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from types import ClassType
22
from types import DictType
23
from types import FileType
24
from types import IntType
25
from types import ListType
26
from types import StringType
27
from types import StringTypes
28
from types import TupleType
29
from types import UnicodeType
30
31
from AccessControl import ClassSecurityInfo
32
from App.class_init import InitializeClass
33
from DateTime import DateTime
34
from Products.Archetypes.atapi import DisplayList
35
from Products.Archetypes.debug import log
36
from Products.Archetypes.Field import ObjectField
37
from Products.Archetypes.Field import decode
38
from Products.Archetypes.Field import encode
39
from Products.Archetypes.Registry import registerField
40
from Products.Archetypes.Registry import registerPropertyType
41
from Products.CMFCore.Expression import Expression
42
from Products.CMFCore.Expression import createExprContext
43
from Products.CMFCore.utils import getToolByName
44
from Products.PythonScripts.standard import html_quote
45
from senaite.core.browser.fields.utils import getDisplayList
46
from senaite.core.browser.widgets.recordwidget import RecordWidget
47
48
# we have to define our own validation handling
49
try:
50
    from Products.validation import ValidationChain
51
    from Products.validation import UnknowValidatorError
52
    from Products.validation import FalseValidatorError
53
    from Products.validation.interfaces.IValidator \
54
         import IValidator, IValidationChain
55
    HAS_VALIDATION_CHAIN = 1
56
except ImportError:
57
    HAS_VALIDATION_CHAIN = 0
58
59
60
def providedBy(interface, obj):
61
    if getattr(interface, 'providedBy', None):
62
        return interface.providedBy(obj)
63
    return interface.isImplementedBy(obj)
64
65
66
class RecordField(ObjectField):
67
    """A field that stores a "record" (dictionary-like) construct"""
68
    _properties = ObjectField._properties.copy()
69
    _properties.update({
70
        "type": "record",
71
        "default": {},
72
        "subfields": (),
73
        "subfield_types": {},
74
        "subfield_vocabularies": {},
75
        "subfield_labels": {},
76
        "subfield_sizes": {},
77
        "subfield_maxlength": {},
78
        "required_subfields": (),
79
        "subfield_validators": {},
80
        "subfield_conditions": {},
81
        "subfield_descriptions": {},
82
        "innerJoin": ", ",
83
        "outerJoin": ", ",
84
        "widget": RecordWidget,
85
        })
86
87
    security = ClassSecurityInfo()
88
89
    security.declarePublic('getSubfields')
90
    def getSubfields(self):
91
        """the tuple of sub-fields"""
92
        return self.subfields
93
94
    security.declarePublic('getSubfieldType')
95
    def getSubfieldType(self, subfield):
96
        """
97
        optional type declaration
98
        default: string
99
        """
100
        return self.subfield_types.get(subfield, 'string')
101
102
    security.declarePublic('getSubfieldLabel')
103
    def getSubfieldLabel(self, subfield):
104
        """
105
        optional custom label for the subfield
106
        default: the id of the subfield
107
        """
108
        return self.subfield_labels.get(subfield, subfield.capitalize())
109
110
    security.declarePublic('getSubfieldDescription')
111
    def getSubfieldDescription(self, subfield):
112
        """
113
        optional custom description for the subfield
114
        """
115
        return self.subfield_descriptions.get(subfield)
116
117
    def getSubfieldSize(self, subfield, default=40):
118
        """
119
        optional custom size for the subfield
120
        default: 40
121
        only effective for string type subfields
122
        """
123
        return self.subfield_sizes.get(subfield, default)
124
125
    def getSubfieldMaxlength(self, subfield):
126
        """
127
        otional custom maxlength size for the subfield
128
        only effective for string type subfields
129
        """
130
        return self.subfield_maxlength.get(subfield, 40)
131
132
    def isRequired(self,subfield):
133
        """
134
        looks whether subfield is included in the list of required subfields
135
        """
136
        return subfield in self.required_subfields
137
138
    def isSelection(self,subfield):
139
        """select box needed?"""
140
141
        return self.subfield_vocabularies.has_key(subfield)
142
143
    security.declarePublic('testSubfieldCondition')
144
    def testSubfieldCondition(self, subfield, folder, portal, object):
145
        """Test the subfield condition."""
146
        try:
147
            condition = self.subfield_conditions.get(subfield, None)
148
            if condition is not None:
149
                __traceback_info__ = (folder, portal, object, condition)
150
                ec = createExprContext(folder, portal, object)
151
                return Expression(condition)(ec)
152
            else:
153
                return True
154
        except AttributeError:
155
            return True
156
157
    def getVocabularyFor(self, subfield, instance=None):
158
        """the vocabulary (DisplayList) for the subfield"""
159
        ## XXX rr: couldn't we just rely on the field's
160
        ## Vocabulary method here?
161
        value = None
162
        vocab = self.subfield_vocabularies.get(subfield, None)
163
        if not vocab:
164
            raise AttributeError("No vocabulary found for {}".format(subfield))
165
166
        if isinstance(vocab, DisplayList):
167
            return vocab
168
169
        if type(vocab) in StringTypes:
170
            value = None
171
            method = getattr(self, vocab, None)
172
            if method and callable(method):
173
                value = method(instance)
174
            else:
175
                if instance is not None:
176
                    method = getattr(instance, vocab, None)
177
                    if method and callable(method):
178
                        value = method()
179
            if not isinstance(value, DisplayList):
180
                msg = "{} is not a DisplayList {}".format(value, subfield)
181
                raise TypeError(msg)
182
            return value
183
184
        msg = "{} is not a string or DisplayList for {}".format(vocab, subfield)
185
        raise TypeError(msg)
186
187
    def getViewFor(self, instance, subfield, joinWith=', '):
188
        """
189
        formatted value of the subfield for display
190
        """
191
        raw = self.getRaw(instance).get(subfield,'')
192
        if type(raw) in (type(()), type([])):
193
            raw = joinWith.join(raw)
194
        # Prevent XSS attacks by quoting all user input
195
        raw = html_quote(str(raw))
196
        # this is now very specific
197
        if subfield == 'email':
198
            return self.hideEmail(raw,instance)
199
        if subfield == 'phone':
200
            return self.labelPhone(raw)
201
        if subfield == 'fax':
202
            return self.labelFax(raw)
203
        if subfield == 'homepage':
204
            return '<a href="%s">%s</a>' % (raw, raw)
205
        return raw
206
207
    def getSubfieldViews(self,instance,joinWith=', '):
208
        """
209
        list of subfield views for non-empty subfields
210
        """
211
        result = []
212
        for subfield in self.getSubfields():
213
            view = self.getViewFor(instance,subfield,joinWith)
214
            if view:
215
                result.append(view)
216
        return result
217
218
    # this is really special purpose and in no ways generic
219
    def hideEmail(self,email='',instance=None):
220
	masked = 'email: ' + \
221
                 email.replace('@', ' (at) ').replace('.', ' (dot) ')
222
	membertool = getToolByName(instance,'portal_membership',None)
223
	if membertool is None or membertool.isAnonymousUser():
224
	    return masked
225
	return "<a href='mailto:%s'>%s</a>" % (email,email)
226
227
    def labelPhone(self,phone=''):
228
        return 'phone: ' + phone
229
230
    def labelFax(self,fax=''):
231
        return 'fax: ' + fax
232
233
    # enable also a string representation of a dictionary
234
    # to be passed in (external edit may need this)
235
    # store string values as unicode
236
237
    def set(self, instance, value, **kwargs):
238
        if type(value) in StringTypes:
239
            try:
240
                value = eval(value)
241
                # more checks to add?
242
            except: # what to catch here?
243
                pass
244
        value = self._to_dict(value)
245
        value = self._decode_strings(value, instance, **kwargs)
246
        ObjectField.set(self, instance, value, **kwargs)
247
248
    def _to_dict(self, value):
249
        if type(value) != type({}) and hasattr(value, 'keys'):
250
            new_value = {}
251
            new_value.update(value)
252
            return new_value
253
        return value
254
255
    def _decode_strings(self, value, instance, **kwargs):
256
        new_value = value
257
        for k, v in value.items():
258
            if type(v) is type(''):
259
                nv =  decode(v, instance, **kwargs)
260
                try:
261
                    new_value[k] = nv
262
                except AttributeError: # Records don't provide __setitem__
263
                    setattr(new_value, k , nv)
264
265
            # convert datetimes
266
            if self.subfield_types.get(k, None) == 'datetime':
267
                try:
268
                    val = DateTime(v)
269
                except:
270
                    val = None
271
272
                new_value[k] = val
273
274
        return new_value
275
276
    # Return strings using the site's encoding
277
278
    def get(self, instance, **kwargs):
279
        value = ObjectField.get(self, instance, **kwargs)
280
        return self._encode_strings(value, instance, **kwargs)
281
282
    def _encode_strings(self, value, instance, **kwargs):
283
        new_value = value
284
        for k, v in value.items():
285
            if type(v) is type(u''):
286
                nv = encode(v, instance, **kwargs)
287
                try:
288
                    new_value[k] = nv
289
                except AttributeError: # Records don't provide __setitem__
290
                    setattr(new_value, k , nv)
291
        return new_value
292
293
    if HAS_VALIDATION_CHAIN:
294
        def _validationLayer(self):
295
            """
296
            Resolve that each validator is in the service. If validator is
297
            not, log a warning.
298
299
            We could replace strings with class refs and keep things impl
300
            the ivalidator in the list.
301
302
            Note: XXX this is not compat with aq_ things like scripts with __call__
303
            """
304
            for subfield in self.getSubfields():
305
                self.subfield_validators[subfield] = self._subfieldValidationLayer(subfield)
306
307
        def _subfieldValidationLayer(self, subfield):
308
            """
309
            for the individual subfields
310
            """
311
            chainname = 'Validator_%s_%s' % (self.getName(), subfield)
312
            current_validators = self.subfield_validators.get(subfield, ())
313
314
            if type(current_validators) is DictType:
315
                msg = "Please use the new syntax with validation chains"
316
                raise NotImplementedError(msg)
317
            elif IValidationChain.providedBy(current_validators):
318
                validators = current_validators
319
            elif IValidator.providedBy(current_validators):
320
                validators = ValidationChain(chainname, validators=current_validators)
321
            elif type(current_validators) in (TupleType, ListType, StringType):
322
                if len(current_validators):
323
                    # got a non empty list or string - create a chain
324
                    try:
325
                        validators = ValidationChain(chainname, validators=current_validators)
326
                    except (UnknowValidatorError, FalseValidatorError) as msg:
327
                        log("WARNING: Disabling validation for %s/%s: %s" % (self.getName(), subfield, msg))
328
                        validators = ()
329
                else:
330
                    validators = ()
331
            else:
332
                log('WARNING: Unknow validation %s. Disabling!' % current_validators)
333
                validators = ()
334
335
            if not subfield in self.required_subfields:
336
                if validators == ():
337
                    validators = ValidationChain(chainname)
338
                if len(validators):
339
                    # insert isEmpty validator at position 0 if first validator
340
                    # is not isEmpty
341
                    if not validators[0][0].name == 'isEmpty':
342
                        validators.insertSufficient('isEmpty')
343
                else:
344
                    validators.insertSufficient('isEmpty')
345
346
            return validators
347
348
        security.declarePublic('validate')
349
        def validate(self, value, instance, errors={}, **kwargs):
350
            """
351
            Validate passed-in value using all subfield validators.
352
            Return None if all validations pass; otherwise, return failed
353
            result returned by validator
354
            """
355
            name = self.getName()
356
            if errors and errors.has_key(name):
357
                return True
358
359
            if self.required_subfields:
360
                for subfield in self.required_subfields:
361
                    sub_value = value.get(subfield, None)
362
                    res = self.validate_required(instance, sub_value, errors)
363
                    if res is not None:
364
                        return res
365
366
            # not touched yet
367
            if self.enforceVocabulary:
368
                res = self.validate_vocabulary(instance, value, errors)
369
                if res is not None:
370
                    return res
371
372
            # can this part stay like it is?
373
            res = instance.validate_field(name, value, errors)
374
            if res is not None:
375
                return res
376
377
            # this should work
378
            if self.subfield_validators:
379
                res = self.validate_validators(value, instance, errors, **kwargs)
380
                if res is not True:
381
                    return res
382
383
            # all ok
384
            return None
385
386
        def validate_validators(self, value, instance, errors, **kwargs):
387
            result = True
388
            total = ''
389
            for subfield in self.getSubfields():
390
                subfield_validators = self.subfield_validators.get(subfield, None)
391
                if subfield_validators:
392
                    result = subfield_validators(value.get(subfield),
393
                                                 instance=instance,
394
                                                 errors=errors,
395
                                                 field=self,
396
                                                 **kwargs
397
                                                 )
398
                if result is not True:
399
                    total += result
400
            return total or result
401
402
403
InitializeClass(RecordField)
404
405
406
registerField(RecordField, title="Record", description="")
407
408
registerPropertyType('subfields', 'lines', RecordField)
409
registerPropertyType('required_subfields', 'lines', RecordField)
410
registerPropertyType('subfield_validators', 'mapping', RecordField)
411
registerPropertyType('subfield_types', 'mapping', RecordField)
412
registerPropertyType('subfield_vocabularies', 'mapping', RecordField)
413
registerPropertyType('subfield_labels', 'mapping', RecordField)
414
registerPropertyType('subfield_sizes', 'mapping', RecordField)
415
registerPropertyType('subfield_maxlength', 'mapping', RecordField)
416
registerPropertyType('innerJoin', 'string', RecordField)
417
registerPropertyType('outerJoin', 'string', RecordField)
418