1import warnings
2
3from tg._compat import unicode_text
4
5from .i18n import _formencode_gettext, lazy_ugettext
6
7try:
8    from tw2.core import ValidationError as _Tw2ValidationError
9except ImportError: #pragma: no cover
10    class _Tw2ValidationError(Exception):
11        """ToscaWidgets2 Validation Error"""
12
13try:
14    from formencode.api import Invalid as _FormEncodeValidationError
15    from formencode.api import Validator as _FormEncodeValidator
16    from formencode import Schema as _FormEncodeSchema
17except ImportError: #pragma: no cover
18    class _FormEncodeValidationError(Exception):
19        """FormEncode Invalid"""
20    class _FormEncodeValidator(object):
21        """FormEncode Validator"""
22    class _FormEncodeSchema(object):
23        """FormEncode Schema"""
24
25
26class _ValidationStatus(object):
27    """Current request parameters validation status.
28
29    Keeps track of currently validated values, errors and
30    the ValidationIntent that caused the validation process.
31    """
32    __slots__ = ('errors', 'values', 'exception', 'intent')
33
34    def __init__(self, errors=None, values=None, exception=None, intent=None):
35        self.errors = errors or {}
36        self.values = values or {}
37        self.exception = exception
38        self.intent = intent
39
40    @property
41    def error_handler(self):
42        if self.intent is None:
43            return None
44        return self.intent.error_handler
45
46    @property
47    def chain_validation(self):
48        if self.intent is None:
49            return False
50        return self.intent.chain_validation
51
52    def __getitem__(self, item):
53        warnings.warn("Accessing validation status properties with [] syntax is deprecated. "
54                      " Please use dot notation instead", DeprecationWarning, stacklevel=2)
55        try:
56            return getattr(self, item)
57        except AttributeError:
58            raise KeyError
59
60
61class _ValidationIntent(object):
62    """Details of validation intention.
63
64    Describes how a validation should happen and how
65    errors should be handled. It also performs the
66    validation itself on the parameters for a given
67    controller method.
68    """
69    def __init__(self, validators, error_handler, chain_validation):
70        self.validators = validators
71        self.error_handler = error_handler
72        self.chain_validation = chain_validation
73
74    def check(self, method, params):
75        validators = self.validators
76        if not validators:
77            return params
78
79        # An object used by FormEncode to get translator function
80        formencode_state = type('state', (), {'_': staticmethod(_formencode_gettext)})
81        validated_params = {}
82
83        # The validator may be a dictionary, a FormEncode Schema object, or any
84        # object with a "validate" method.
85        if isinstance(validators, dict):
86            # TG developers can pass in a dict of param names and FormEncode
87            # validators.  They are applied one by one and builds up a new set
88            # of validated params.
89
90            errors = {}
91            for field, validator in validators.items():
92                try:
93                    if isinstance(validator, _FormEncodeValidator):
94                        validated_params[field] = validator.to_python(params.get(field), formencode_state)
95                    else:
96                        validated_params[field] = validator.to_python(params.get(field))
97                # catch individual validation errors into the errors dictionary
98                except validation_errors as inv:
99                    errors[field] = inv
100
101            # Parameters that don't have validators are returned verbatim
102            for param, param_value in params.items():
103                if param not in validated_params:
104                    validated_params[param] = param_value
105
106            # If there are errors, create a compound validation error based on
107            # the errors dictionary, and raise it as an exception
108            if errors:
109                raise TGValidationError(TGValidationError.make_compound_message(errors),
110                                        value=params,
111                                        error_dict=errors)
112
113        elif isinstance(validators, _FormEncodeSchema):
114            # A FormEncode Schema object - to_python converts the incoming
115            # parameters to sanitized Python values
116            validated_params = validators.to_python(params, formencode_state)
117
118        elif hasattr(validators, 'validate') and getattr(self, 'needs_controller', False):
119            # An object with a "validate" method - call it with the parameters
120            validated_params = validators.validate(method, params, formencode_state)
121
122        elif hasattr(validators, 'validate'):
123            # An object with a "validate" method - call it with the parameters
124            validated_params = validators.validate(params, formencode_state)
125
126        return validated_params
127
128
129def _navigate_tw2form_children(w):
130    if getattr(w, 'compound_key', None):
131        # If we have a compound_key it's a leaf widget with form values
132        yield w
133    else:
134        child = getattr(w, 'child', None)
135        if child:
136            # Widgets with "child" don't have children, but their child has
137            w = child
138
139        for c in getattr(w, 'children', []):
140            for cc in _navigate_tw2form_children(c):
141                yield cc
142
143
144class TGValidationError(Exception):
145    """Invalid data was encountered during validation.
146
147    The constructor can be passed a short message with
148    the reason of the failed validation.
149    """
150    def __init__(self, msg, value=None, error_dict=None):
151        super(TGValidationError, self).__init__(msg)
152        self.msg = msg
153        self.value = value
154        self.error_dict = error_dict
155
156    @classmethod
157    def make_compound_message(cls, error_dict):
158        return unicode_text('\n').join(
159            unicode_text("%s: %s") % errorinfo for errorinfo in error_dict.items()
160        )
161
162    def __str__(self):
163        return str(self.msg)
164
165    def __unicode__(self):
166        return unicode_text(self.msg)
167
168
169class Convert(object):
170    """Applies a conversion function as a validator.
171
172    This is meant to implement simple validation mechanism.
173
174    Any callable can be used for ``func`` as far as it accepts an argument and
175    returns the converted object. In case of exceptions the validation
176    is considered failed and the ``msg`` parameter is displayed as
177    an error.
178
179    A ``default`` value can be provided for values that are missing
180    (evaluate to false) which will be used in place of the missing value.
181
182    Example::
183
184        @expose()
185        @validate({
186            'num': Convert(int, 'Must be a number')
187        }, error_handler=insert_number)
188        def post_pow2(self, num):
189            return str(num*num)
190    """
191    def __init__(self, func, msg=lazy_ugettext('Invalid'), default=None):
192        self._func = func
193        self._msg = msg
194        self._default = default
195
196    def to_python(self, value, state=None):
197        value = value or self._default
198
199        try:
200            return self._func(value)
201        except:
202            raise TGValidationError(self._msg, value)
203
204
205validation_errors = (_Tw2ValidationError, _FormEncodeValidationError, TGValidationError)
206