1# -*- test-case-name: twisted.test.test_formmethod -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5
6"""
7Form-based method objects.
8
9This module contains support for descriptive method signatures that can be used
10to format methods.
11"""
12
13import calendar
14
15class FormException(Exception):
16    """An error occurred calling the form method.
17    """
18    def __init__(self, *args, **kwargs):
19        Exception.__init__(self, *args)
20        self.descriptions = kwargs
21
22
23class InputError(FormException):
24    """
25    An error occurred with some input.
26    """
27
28
29class Argument:
30    """Base class for form arguments."""
31
32    # default value for argument, if no other default is given
33    defaultDefault = None
34
35    def __init__(self, name, default=None, shortDesc=None,
36                 longDesc=None, hints=None, allowNone=1):
37        self.name = name
38        self.allowNone = allowNone
39        if default is None:
40            default = self.defaultDefault
41        self.default = default
42        self.shortDesc = shortDesc
43        self.longDesc = longDesc
44        if not hints:
45            hints = {}
46        self.hints = hints
47
48    def addHints(self, **kwargs):
49        self.hints.update(kwargs)
50
51    def getHint(self, name, default=None):
52        return self.hints.get(name, default)
53
54    def getShortDescription(self):
55        return self.shortDesc or self.name.capitalize()
56
57    def getLongDescription(self):
58        return self.longDesc or '' #self.shortDesc or "The %s." % self.name
59
60    def coerce(self, val):
61        """Convert the value to the correct format."""
62        raise NotImplementedError("implement in subclass")
63
64
65class String(Argument):
66    """A single string.
67    """
68    defaultDefault = ''
69    min = 0
70    max = None
71
72    def __init__(self, name, default=None, shortDesc=None,
73                 longDesc=None, hints=None, allowNone=1, min=0, max=None):
74        Argument.__init__(self, name, default=default, shortDesc=shortDesc,
75                          longDesc=longDesc, hints=hints, allowNone=allowNone)
76        self.min = min
77        self.max = max
78
79    def coerce(self, val):
80        s = str(val)
81        if len(s) < self.min:
82            raise InputError("Value must be at least %s characters long" % self.min)
83        if self.max != None and len(s) > self.max:
84            raise InputError("Value must be at most %s characters long" % self.max)
85        return str(val)
86
87
88class Text(String):
89    """A long string.
90    """
91
92
93class Password(String):
94    """A string which should be obscured when input.
95    """
96
97
98class VerifiedPassword(String):
99    """A string that should be obscured when input and needs verification."""
100
101    def coerce(self, vals):
102        if len(vals) != 2 or vals[0] != vals[1]:
103            raise InputError("Please enter the same password twice.")
104        s = str(vals[0])
105        if len(s) < self.min:
106            raise InputError("Value must be at least %s characters long" % self.min)
107        if self.max != None and len(s) > self.max:
108            raise InputError("Value must be at most %s characters long" % self.max)
109        return s
110
111
112class Hidden(String):
113    """A string which is not displayed.
114
115    The passed default is used as the value.
116    """
117
118
119class Integer(Argument):
120    """A single integer.
121    """
122    defaultDefault = None
123
124    def __init__(self, name, allowNone=1, default=None, shortDesc=None,
125                 longDesc=None, hints=None):
126        #although Argument now has allowNone, that was recently added, and
127        #putting it at the end kept things which relied on argument order
128        #from breaking.  However, allowNone originally was in here, so
129        #I have to keep the same order, to prevent breaking code that
130        #depends on argument order only
131        Argument.__init__(self, name, default, shortDesc, longDesc, hints,
132                          allowNone)
133
134    def coerce(self, val):
135        if not val.strip() and self.allowNone:
136            return None
137        try:
138            return int(val)
139        except ValueError:
140            raise InputError("%s is not valid, please enter a whole number, e.g. 10" % val)
141
142
143class IntegerRange(Integer):
144
145    def __init__(self, name, min, max, allowNone=1, default=None, shortDesc=None,
146                 longDesc=None, hints=None):
147        self.min = min
148        self.max = max
149        Integer.__init__(self, name, allowNone=allowNone, default=default, shortDesc=shortDesc,
150                         longDesc=longDesc, hints=hints)
151
152    def coerce(self, val):
153        result = Integer.coerce(self, val)
154        if self.allowNone and result == None:
155            return result
156        if result < self.min:
157            raise InputError("Value %s is too small, it should be at least %s" % (result, self.min))
158        if result > self.max:
159            raise InputError("Value %s is too large, it should be at most %s" % (result, self.max))
160        return result
161
162
163class Float(Argument):
164
165    defaultDefault = None
166
167    def __init__(self, name, allowNone=1, default=None, shortDesc=None,
168                 longDesc=None, hints=None):
169        #although Argument now has allowNone, that was recently added, and
170        #putting it at the end kept things which relied on argument order
171        #from breaking.  However, allowNone originally was in here, so
172        #I have to keep the same order, to prevent breaking code that
173        #depends on argument order only
174        Argument.__init__(self, name, default, shortDesc, longDesc, hints,
175                          allowNone)
176
177
178    def coerce(self, val):
179        if not val.strip() and self.allowNone:
180            return None
181        try:
182            return float(val)
183        except ValueError:
184            raise InputError("Invalid float: %s" % val)
185
186
187class Choice(Argument):
188    """
189    The result of a choice between enumerated types.  The choices should
190    be a list of tuples of tag, value, and description.  The tag will be
191    the value returned if the user hits "Submit", and the description
192    is the bale for the enumerated type.  default is a list of all the
193    values (seconds element in choices).  If no defaults are specified,
194    initially the first item will be selected.  Only one item can (should)
195    be selected at once.
196    """
197    def __init__(self, name, choices=[], default=[], shortDesc=None,
198                 longDesc=None, hints=None, allowNone=1):
199        self.choices = choices
200        if choices and not default:
201            default.append(choices[0][1])
202        Argument.__init__(self, name, default, shortDesc, longDesc, hints, allowNone=allowNone)
203
204    def coerce(self, inIdent):
205        for ident, val, desc in self.choices:
206            if ident == inIdent:
207                return val
208        else:
209            raise InputError("Invalid Choice: %s" % inIdent)
210
211
212class Flags(Argument):
213    """
214    The result of a checkbox group or multi-menu.  The flags should be a
215    list of tuples of tag, value, and description. The tag will be
216    the value returned if the user hits "Submit", and the description
217    is the bale for the enumerated type.  default is a list of all the
218    values (second elements in flags).  If no defaults are specified,
219    initially nothing will be selected.  Several items may be selected at
220    once.
221    """
222    def __init__(self, name, flags=(), default=(), shortDesc=None,
223                 longDesc=None, hints=None, allowNone=1):
224        self.flags = flags
225        Argument.__init__(self, name, default, shortDesc, longDesc, hints, allowNone=allowNone)
226
227    def coerce(self, inFlagKeys):
228        if not inFlagKeys:
229            return []
230        outFlags = []
231        for inFlagKey in inFlagKeys:
232            for flagKey, flagVal, flagDesc in self.flags:
233                if inFlagKey == flagKey:
234                    outFlags.append(flagVal)
235                    break
236            else:
237                raise InputError("Invalid Flag: %s" % inFlagKey)
238        return outFlags
239
240
241class CheckGroup(Flags):
242    pass
243
244
245class RadioGroup(Choice):
246    pass
247
248
249class Boolean(Argument):
250    def coerce(self, inVal):
251        if not inVal:
252            return 0
253        lInVal = str(inVal).lower()
254        if lInVal in ('no', 'n', 'f', 'false', '0'):
255            return 0
256        return 1
257
258class File(Argument):
259    def __init__(self, name, allowNone=1, shortDesc=None, longDesc=None,
260                 hints=None):
261        Argument.__init__(self, name, None, shortDesc, longDesc, hints,
262                          allowNone=allowNone)
263
264    def coerce(self, file):
265        if not file and self.allowNone:
266            return None
267        elif file:
268            return file
269        else:
270            raise InputError("Invalid File")
271
272def positiveInt(x):
273    x = int(x)
274    if x <= 0: raise ValueError
275    return x
276
277class Date(Argument):
278    """A date -- (year, month, day) tuple."""
279
280    defaultDefault = None
281
282    def __init__(self, name, allowNone=1, default=None, shortDesc=None,
283                 longDesc=None, hints=None):
284        Argument.__init__(self, name, default, shortDesc, longDesc, hints)
285        self.allowNone = allowNone
286        if not allowNone:
287            self.defaultDefault = (1970, 1, 1)
288
289    def coerce(self, args):
290        """Return tuple of ints (year, month, day)."""
291        if tuple(args) == ("", "", "") and self.allowNone:
292            return None
293
294        try:
295            year, month, day = map(positiveInt, args)
296        except ValueError:
297            raise InputError("Invalid date")
298        if (month, day) == (2, 29):
299            if not calendar.isleap(year):
300                raise InputError("%d was not a leap year" % year)
301            else:
302                return year, month, day
303        try:
304            mdays = calendar.mdays[month]
305        except IndexError:
306            raise InputError("Invalid date")
307        if day > mdays:
308            raise InputError("Invalid date")
309        return year, month, day
310
311
312class Submit(Choice):
313    """Submit button or a reasonable facsimile thereof."""
314
315    def __init__(self, name, choices=[("Submit", "submit", "Submit form")],
316                 reset=0, shortDesc=None, longDesc=None, allowNone=0, hints=None):
317        Choice.__init__(self, name, choices=choices, shortDesc=shortDesc,
318                        longDesc=longDesc, hints=hints)
319        self.allowNone = allowNone
320        self.reset = reset
321
322    def coerce(self, value):
323        if self.allowNone and not value:
324            return None
325        else:
326            return Choice.coerce(self, value)
327
328
329class PresentationHint:
330    """
331    A hint to a particular system.
332    """
333
334
335class MethodSignature:
336
337    def __init__(self, *sigList):
338        """
339        """
340        self.methodSignature = sigList
341
342    def getArgument(self, name):
343        for a in self.methodSignature:
344            if a.name == name:
345                return a
346
347    def method(self, callable, takesRequest=False):
348        return FormMethod(self, callable, takesRequest)
349
350
351class FormMethod:
352    """A callable object with a signature."""
353
354    def __init__(self, signature, callable, takesRequest=False):
355        self.signature = signature
356        self.callable = callable
357        self.takesRequest = takesRequest
358
359    def getArgs(self):
360        return tuple(self.signature.methodSignature)
361
362    def call(self,*args,**kw):
363        return self.callable(*args,**kw)
364