Introduced "MaskedEmail" and allow a masked email in the Gasthausbox.
[philipp/winterrodeln/wrpylib.git] / wrpylib / wrvalidators.py
1 #!/usr/bin/python2.6
2 # -*- coding: iso-8859-15 -*-
3 # $Id$
4 # $HeadURL$
5 """This file contains "validators" that convert between string and python (database) representation
6 of properties used in the "Rodelbahnbox" and "Gasthausbox".
7 The "to_python" method has to get a unicode argument.
8 """
9 import formencode
10 import formencode.national
11 import datetime
12 import re
13 import xml.dom.minidom as minidom
14 from xml.parsers.expat import ExpatError
15
16
17 class NoneValidator(formencode.FancyValidator):
18     """Takes a validator and makes it possible that empty strings are mapped to None."""
19     def __init__(self, validator, python_none=None):
20         self.validator = validator
21         self.python_none = python_none
22     
23     def to_python(self, value):
24         self.assert_string(value, None)
25         if value == u'': return self.python_none
26         return self.validator.to_python(value)
27     
28     def from_python(self, value):
29         if value == self.python_none: return u''
30         return self.validator.from_python(value)
31
32
33 class NeinValidator(formencode.FancyValidator):
34     """Take an arbitrary validator and adds the possibility that the
35     string can be u'Nein'.
36     Example together with an UnsignedNone validator:
37     >>> v = NeinValidator(UnsignedNone())
38     >>> v.to_python(u'')
39     None
40     >>> v.to_python(u'34')
41     34
42     >>> v.to_python(u'Nein')
43     u'Nein'
44     """
45     def __init__(self, validator, python_no=u'Nein'):
46         self.validator = validator
47         self.python_no = python_no
48     
49     def to_python(self, value):
50         self.assert_string(value, None)
51         if value == u'Nein': return self.python_no
52         return self.validator.to_python(value)
53     
54     def from_python(self, value):
55         if value == self.python_no: return u'Nein'
56         return self.validator.from_python(value)
57
58
59 class Unicode(formencode.FancyValidator):
60     """Converts an unicode string to an unicode string:
61     u'any string' <=> u'any string'"""
62     def to_python(self, value):
63         self.assert_string(value, None)
64         return unicode(value)
65
66     def from_python(self, value):
67         return unicode(value)
68
69
70 class UnicodeNone(NoneValidator):
71     """Converts an unicode string to an unicode string:
72     u'' <=> None
73     u'any string' <=> u'any string'"""
74     def __init__(self):
75         NoneValidator.__init__(self, Unicode())
76
77
78 class Unsigned(formencode.FancyValidator):
79     """Converts an unsigned number to a string and vice versa:
80     u'0'  <=>  0
81     u'1'  <=>  1
82     u'45' <=> 45
83     """
84     def __init__(self, max=None):
85         self.iv = formencode.validators.Int(min=0, max=max)
86
87     def to_python(self, value):
88         self.assert_string(value, None)
89         return self.iv.to_python(value)
90     
91     def from_python(self, value):
92         return unicode(value)
93
94
95 class UnsignedNone(NoneValidator):
96     """Converts an unsigned number to a string and vice versa:
97     u''   <=> None
98     u'0'  <=>  0
99     u'1'  <=>  1
100     u'45' <=> 45
101     """
102     def __init__(self, max=None):
103         NoneValidator.__init__(self, Unsigned(max))
104
105
106 class UnsignedNeinNone(NoneValidator):
107     """ Translates a number of Nein to a number.
108     u''     <=> None
109     u'Nein' <=> 0
110     u'1'    <=> 1
111     u'2'    <=> 2
112     ...
113     """
114     def __init__(self):
115         NoneValidator.__init__(self, UnsignedNone())
116
117
118 class Loop(formencode.FancyValidator):
119     """Takes a validator and calls from_python(to_python(value))."""
120     def __init__(self, validator):
121         self.validator = validator
122
123     def to_python(self, value):
124         self.assert_string(value, None)
125         return self.validator.from_python(self.validator.to_python(value))
126     
127     def from_python(self, value):
128         # we don't call self.validator.to_python(self.validator.from_python(value))
129         # here because our to_python implementation basically leaves the input untouched
130         # and so should from_python do.
131         return self.validator.from_python(self.validator.to_python(value))
132
133
134 class DictValidator(formencode.FancyValidator):
135     """Translates strings to other values via a python directory.
136     >>> boolValidator = DictValidator({u'': None, u'Ja': True, u'Nein': False})
137     >>> boolValidator.to_python(u'')
138     None
139     >>> boolValidator.to_python(u'Ja')
140     True
141     """
142     def __init__(self, dict):
143         self.dict = dict
144     
145     def to_python(self, value):
146         self.assert_string(value, None)
147         if not self.dict.has_key(value): raise formencode.Invalid("Key not found in dict.", value, None)
148         return self.dict[value]
149     
150     def from_python(self, value):
151         for k, v in self.dict.iteritems():
152             if type(v) == type(value) and v == value: return k
153         raise formencode.Invalid('Invalid value', value, None)
154
155
156 class GermanBoolNone(DictValidator):
157     """Converts German bool values to the python bool type:
158     u''     <=> None
159     u'Ja'   <=> True
160     u'Nein' <=> False
161     """
162     def __init__(self):
163         DictValidator.__init__(self, {u'': None, u'Ja': True, u'Nein': False})
164
165
166 class GermanTristateTuple(DictValidator):
167     """Does the following conversion:
168     u''          <=> (None, None)
169     u'Ja'        <=> (True, False)
170     u'Teilweise' <=> (True,  True)
171     u'Nein'      <=> (False, True)"""
172     def __init__(self, yes_python = (True, False), no_python = (False, True), partly_python = (True, True), none_python = (None, None)):
173         DictValidator.__init__(self, {u'': none_python, u'Ja': yes_python, u'Nein': no_python, u'Teilweise': partly_python})
174
175
176 class GermanTristateFloat(GermanTristateTuple):
177     """Converts the a property with the possible values 0.0, 0.5, 1.0 or None
178     to a German text:
179     u''          <=> None
180     u'Ja'        <=> 1.0
181     u'Teilweise' <=> 0.5
182     u'Nein'      <=> 0.0"""
183     def __init__(self):
184         GermanTristateTuple.__init__(self, yes_python=1.0, no_python=0.0, partly_python=0.5, none_python=None)
185
186
187 class ValueComment(formencode.FancyValidator):
188     """Converts value with a potentially optional comment to a python tuple. If a comment is present, the
189     closing bracket has to be the rightmost character.
190     u''                                 <=> (None, None)
191     u'value'                            <=> (u'value', None)
192     u'value (comment)'                  <=> (u'value', u'comment')
193     u'[[link (linkcomment)]]'           <=> (u'[[link (linkcomment)]]', None)
194     u'[[link (linkcomment)]] (comment)' <=> (u'[[link (linkcomment)]]', comment)
195     """
196     def __init__(self, value_validator=UnicodeNone(), comment_validator=UnicodeNone(), comment_is_optional=True):
197         self.value_validator = value_validator
198         self.comment_validator = comment_validator
199         self.comment_is_optional = comment_is_optional
200     
201     def to_python(self, value):
202         self.assert_string(value, None)
203         if value == u'':
204             v = value
205             c = value
206         else:
207             right = value.rfind(')')
208             if right+1 != len(value):
209                 if not self.comment_is_optional: raise formencode.Invalid(u'Mandatory comment not present', value, None)
210                 v = value
211                 c = u''
212             else:
213                 left = value.rfind('(')
214                 if left < 0: raise formencode.Invalid(u'Invalid format', value, None)
215                 v = value[:left].strip()
216                 c = value[left+1:right].strip()
217         return self.value_validator.to_python(v), self.comment_validator.to_python(c)
218
219     def from_python(self, value):
220         assert len(value) == 2
221         v = self.value_validator.from_python(value[0])
222         c = self.comment_validator.from_python(value[1])
223         if len(c) > 0:
224             if len(v) > 0: return u'%s (%s)' % (v, c)
225             else: return u'(%s)' % c
226         return v
227
228
229 class SemicolonList(formencode.FancyValidator):
230     """Applies a given validator to a semicolon separated list of values and returns a python list.
231     For an empty string an empty list is returned."""
232     def __init__(self, validator=Unicode()):
233         self.validator = validator
234     
235     def to_python(self, value):
236         self.assert_string(value, None)
237         return [self.validator.to_python(s.strip()) for s in value.split(';')]
238     
239     def from_python(self, value):
240         return "; ".join([self.validator.from_python(s) for s in value])
241         
242     
243 class ValueCommentList(SemicolonList):
244     """A value-comment list looks like one of the following lines:
245         value
246         value (optional comment)
247         value1; value2
248         value1; value2 (optional comment)
249         value1 (optional comment1); value2 (optional comment2); value3 (otional comment3)
250         value1 (optional comment1); value2 (optional comment2); value3 (otional comment3)
251     This function returns the value-comment list as list of tuples:
252         [(u'value1', u'comment1'), (u'value2', None)]
253     If no comment is present, None is specified.
254     For an empty string, [] is returned."""    
255     def __init__(self, value_validator=Unicode(), comments_are_optional=True):
256         SemicolonList.__init__(self, ValueComment(value_validator, comment_is_optional=comments_are_optional))
257
258
259 class GenericDateTime(formencode.FancyValidator):
260     """Converts a generic date/time information to a datetime class with a user defined format.
261     '2009-03-22 20:36:15' would be specified as '%Y-%m-%d %H:%M:%S'."""
262     
263     def __init__(self, date_time_format = '%Y-%m-%d %H:%M:%S', **keywords):
264         formencode.FancyValidator.__init__(self, **keywords)
265         self.date_time_format = date_time_format
266     
267     def to_python(self, value, state=None):
268         self.assert_string(value, None)
269         try: return datetime.datetime.strptime(value, self.date_time_format)
270         except ValueError, e: raise formencode.Invalid(str(e), value, None)
271     
272     def from_python(self, value, state=None):
273         return value.strftime(self.date_time_format)
274
275
276 class DateTimeNoSec(GenericDateTime):
277     def __init__(self, **keywords):
278         GenericDateTime.__init__(self, '%Y-%m-%d %H:%M', **keywords)
279
280
281 class DateNone(NoneValidator):
282     """Converts date information to date classes with the format '%Y-%m-%d' or None."""
283     def __init__(self):
284         NoneValidator.__init__(self, GenericDateTime('%Y-%m-%d'))
285
286
287 class Geo(formencode.FancyValidator):
288     """Formats to coordinates '47.076207 N 11.453553 E' to the (latitude, longitude) tuplet."""
289     def to_python(self, value):
290         self.assert_string(value, None)
291         r = re.match(u'(\d+\.\d+) N (\d+\.\d+) E', value)
292         if r is None: raise formencode.Invalid(u"Coordinates '%s' have not a format like '47.076207 N 11.453553 E'" % value, value, None)
293         return (float(r.groups()[0]), float(r.groups()[1]))
294     
295     def from_python(self, value):
296         latitude, longitude = value
297         return u'%.6f N %.6f E' % (latitude, longitude)
298
299
300 class GeoNone(NoneValidator):
301     """Formats to coordinates '47.076207 N 11.453553 E' to the (latitude, longitude) tuplet."""
302     def __init__(self):
303         NoneValidator.__init__(self, Geo(), (None, None))
304
305
306 class MultiGeo(formencode.FancyValidator):
307     "Formats multiple coordinates, even in multiple lines to [(latitude, longitude, elevation), ...] or [(latitude, longitude, None), ...] tuplets."
308     
309     # Valid for input_format
310     FORMAT_GUESS = 0         # guesses the input format; default for input_format
311     FORMAT_NONE = -1          # indicates missing formats
312     
313     # Valid for input_format and output_format
314     FORMAT_GEOCACHING = 1    # e.g. "N 47° 13.692 E 011° 25.535"
315     FORMAT_WINTERRODELN = 2  # e.g. "47.222134 N 11.467211 E"
316     FORMAT_GMAPPLUGIN = 3    # e.g. "47.232922, 11.452239"
317     FORMAT_GPX = 4           # e.g. "<trkpt lat="47.181289" lon="11.408827"><ele>1090.57</ele></trkpt>"
318     
319     input_format = FORMAT_GUESS
320     output_format = FORMAT_WINTERRODELN
321     last_input_format = FORMAT_NONE
322
323     def __init__(self, input_format = FORMAT_GUESS, output_format = FORMAT_WINTERRODELN, **keywords):
324         self.input_format = input_format
325         self.output_format = output_format
326         formencode.FancyValidator.__init__(self, if_empty = (None, None, None), **keywords)
327     
328     def to_python(self, value):
329         self.assert_string(value, None)
330         input_format = self.input_format
331         if not input_format in [self.FORMAT_GUESS, self.FORMAT_GEOCACHING, self.FORMAT_WINTERRODELN, self.FORMAT_GMAPPLUGIN, self.FORMAT_GPX]:
332             raise formencode.Invalid(u"input_format %d is not recognized" % input_format, value, None) # Shouldn't it be an other type of runtime error?
333         lines = [line.strip() for line in value.split("\n") if len(line.strip()) > 0]
334         
335         result = []
336         for line in lines:
337             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GEOCACHING:
338                 r = re.match(u'N ?(\d+)° ?(\d+\.\d+) +E ?(\d+)° ?(\d+\.\d+)', line)
339                 if not r is None:
340                     g = r.groups()
341                     result.append((float(g[0]) + float(g[1])/60, float(g[2]) + float(g[3])/60, None))
342                     last_input_format = self.FORMAT_WINTERRODELN
343                     continue
344                     
345             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_WINTERRODELN:
346                 r = re.match(u'(\d+\.\d+) N (\d+\.\d+) E', line)
347                 if not r is None:
348                     result.append((float(r.groups()[0]), float(r.groups()[1]), None))
349                     last_input_format = self.FORMAT_WINTERRODELN
350                     continue
351                 
352             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GMAPPLUGIN:
353                 r = re.match(u'(\d+\.\d+), ?(\d+\.\d+)', line)
354                 if not r is None:
355                     result.append((float(r.groups()[0]), float(r.groups()[1]), None))
356                     last_input_format = self.FORMAT_GMAPPLUGIN
357                     continue
358                 
359             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GPX:
360                 try:
361                     xml = minidom.parseString(line)
362                     coord = xml.documentElement
363                     lat = float(coord.getAttribute('lat'))
364                     lon = float(coord.getAttribute('lon'))
365                     try: ele = float(coord.childNodes[0].childNodes[0].nodeValue)
366                     except (IndexError, ValueError): ele = None
367                     result.append((lat, lon, ele))
368                     last_input_format = self.FORMAT_GPX
369                     continue
370                 except (ExpatError, IndexError, ValueError): pass
371
372             raise formencode.Invalid(u"Coordinates '%s' have no known format" % line, value, None)
373             
374         return result
375     
376     def from_python(self, value):
377         output_format = self.output_format
378         result = []
379         for latitude, longitude, height in value:
380             if output_format == self.FORMAT_GEOCACHING:
381                 degree = latitude
382                 result.append(u'N %02d° %02.3f E %03d° %02.3f' % (latitude, latitude % 1 * 60, longitude, longitude % 1 * 60))
383                 
384             elif output_format == self.FORMAT_WINTERRODELN:
385                 result.append(u'%.6f N %.6f E' % (latitude, longitude))
386
387             elif output_format == self.FORMAT_GMAPPLUGIN:
388                 result.append(u'%.6f, %.6f' % (latitude, longitude))
389                 
390             elif output_format == self.FORMAT_GPX:
391                 if not height is None: result.append(u'<trkpt lat="%.6f" lon="%.6f"><ele>%.2f</ele></trkpt>' % (latitude, longitude, height))
392                 else: result.append(u'<trkpt lat="%.6f" lon="%.6f"/>' % (latitude, longitude))
393             
394             else:
395                 raise formencode.Invalid(u"output_format %d is not recognized" % output_format, value, None) # Shouldn't it be an other type of runtime error?
396             
397         return "\n".join(result)
398
399
400 # deprecated
401 class AustrianPhoneNumber(formencode.FancyValidator):
402     """
403     Validates and converts phone numbers to +##/###/####### or +##/###/#######-### (having an extension)
404     @param  default_cc      country code for prepending if none is provided, defaults to 43 (Austria)
405     ::
406         >>> v = AustrianPhoneNumber()
407         >>> v.to_python(u'0512/12345678')
408         u'+43/512/12345678'
409         >>> v.to_python(u'+43/512/12345678')
410         u'+43/512/12345678'
411         >>> v.to_python(u'0512/1234567-89') # 89 is the extension
412         u'+43/512/1234567-89'
413         >>> v.to_python(u'+43/512/1234567-89')
414         u'+43/512/1234567-89'
415         >>> v.to_python(u'0512 / 12345678') # Exception
416         >>> v.to_python(u'0512-12345678') # Exception
417     """
418     # Inspired by formencode.national.InternationalPhoneNumber
419
420     default_cc = 43 # Default country code
421     messages = {'phoneFormat': "'%%(value)s' is an invalid format. Please enter a number in the form +43/###/####### or 0###/########."}
422
423     def to_python(self, value):
424         self.assert_string(value, None)
425         m = re.match(u'^(?:\+(\d+)/)?([\d/]+)(?:-(\d+))?$', value)
426         # This will separate 
427         #     u'+43/512/1234567-89'  => (u'43', u'512/1234567', u'89')
428         #     u'+43/512/1234/567-89' => (u'43', u'512/1234/567', u'89')
429         #     u'+43/512/1234/567'    => (u'43', u'512/1234/567', None)
430         #     u'0512/1234567'        => (None, u'0512/1234567', None)
431         if m is None: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None)
432         (country, phone, extension) = m.groups()
433         
434         # Phone
435         if phone.find(u'//') > -1 or phone.count('/') == 0: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None)
436         
437         # Country
438         if country is None:
439             if phone[0] != '0': raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None)
440             phone = phone[1:]
441             country = unicode(self.default_cc)
442         
443         if extension is None: return '+%s/%s' % (country, phone)
444         return '+%s/%s-%s' % (country, phone, extension)
445
446
447 # Deprecated
448 class AustrianPhoneNumberNone(NoneValidator):
449     def __init__(self):
450         NoneValidator.__init__(self, AustrianPhoneNumber())
451
452
453 # Deprecated
454 class AustrianPhoneNumberCommentLoop(NoneValidator):
455     def __init__(self):
456         NoneValidator.__init__(self, Loop(ValueComment(AustrianPhoneNumber())))
457
458
459 class GermanDifficulty(DictValidator):
460     """Converts the difficulty represented in a number from 1 to 3 (or None)
461     to a German representation:
462     u''       <=> None
463     u'leicht' <=> 1
464     u'mittel' <=> 2
465     u'schwer' <=> 3"""
466     def __init__(self):
467         DictValidator.__init__(self, {u'': None, u'leicht': 1, u'mittel': 2, u'schwer': 3})
468
469
470 class GermanAvalanches(DictValidator):
471     """Converts the avalanches property represented as number from 1 to 4 (or None)
472     to a German representation:
473     u''             <=> None
474     u'kaum'         <=> 1
475     u'selten'       <=> 2
476     u'gelegentlich' <=> 3
477     u'häufig'       <=> 4"""
478     def __init__(self):
479         DictValidator.__init__(self, {u'': None, u'kaum': 1, u'selten': 2, u'gelegentlich': 3, u'häufig': 4})
480
481
482 class GermanPublicTransport(DictValidator):
483     """Converts the public_transport property represented as number from 1 to 6 (or None)
484     to a German representation:
485     u''            <=> None
486     u'Sehr gut'    <=> 1
487     u'Gut'         <=> 2
488     u'Mittelmäßig' <=> 3
489     u'Schlecht'    <=> 4
490     u'Nein'        <=> 5
491     u'Ja'          <=> 6"""
492     def __init__(self):
493         DictValidator.__init__(self, {u'': None, u'Sehr gut': 1, u'Gut': 2, u'Mittelmäßig': 3, u'Schlecht': 4, u'Nein': 5, u'Ja': 6})
494
495
496 class GermanTristateFloatComment(ValueComment):
497     """Converts the a property with the possible values 0.0, 0.5, 1.0 or None and an optional comment
498     in parenthesis to a German text:
499     u''                  <=> (None, None)
500     u'Ja'                <=> (1.0,  None)
501     u'Teilweise'         <=> (0.5,  None)
502     u'Nein'              <=> (0.0,  None)
503     u'Ja (aber schmal)'  <=> (1.0,  u'aber schmal')
504     u'Teilweise (oben)'  <=> (0.5,  u'oben')
505     u'Nein (aber breit)' <=> (0.0,  u'aber breit')
506     """
507     def __init__(self):
508         ValueComment.__init__(self, GermanTristateFloat())
509
510
511 class UnsignedCommentNone(NoneValidator):
512     """Converts the a property with unsigned values an optional comment
513     in parenthesis to a text:
514     u''           <=> (None, None)
515     u'2 (Mo, Di)' <=> (2,  u'Mo, Di')
516     u'7'          <=> (7,  None)
517     u'0'          <=> (0,  None)
518     """
519     def __init__(self, max=None):
520         NoneValidator.__init__(self, ValueComment(Unsigned(max=max)), (None, None))
521
522
523 class GermanCachet(formencode.FancyValidator):
524     """Converts a "Gütesiegel":
525     u'' <=> None
526     u'Nein' <=> 'Nein'
527     u'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' <=> u'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'"""
528     def to_python(self, value):
529         self.assert_string(value, None)
530         if value == u'': return None
531         elif value == u'Nein': return value
532         elif value.startswith(u'Tiroler Naturrodelbahn-Gütesiegel '):
533             p = value.split(" ")
534             Unsigned().to_python(p[2]) # check if year can be parsed
535             if not p[3] in ['leicht', 'mittel', 'schwer']: raise formencode.Invalid("Unbekannter Schwierigkeitsgrad", value, None)
536             return value
537         else: raise formencode.Invalid(u"Unbekanntes Gütesiegel", value, None)
538     
539     def from_python(self, value):
540         if value == None: return u''
541         assert value != u''
542         return self.to_python(value)
543
544
545 class Url(formencode.FancyValidator):
546     """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed."""
547     urlv = formencode.validators.URL()    
548     def to_python(self, value):
549         self.assert_string(value, None)
550         v = value
551         v = v.replace(u'ä', u'a')
552         v = v.replace(u'ö', u'o')
553         v = v.replace(u'ü', u'u')
554         v = v.replace(u'ß', u'ss')
555         v = self.urlv.to_python(v)
556         return value
557     
558     def from_python(self, value):
559         return value
560
561
562 class UrlNeinNone(NoneValidator):
563     """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed.
564     The special value u"Nein" is allowed."""
565     def __init__(self):
566         NoneValidator.__init__(self, NeinValidator(Url()))
567
568
569 class ValueCommentListNeinLoopNone(NoneValidator):
570     """Translates a semicolon separated list of values with optional comments in paranthesis or u'Nein' to itself.
571     An empty string is translated to None:
572     u''                   <=> None
573     u'Nein'               <=> u'Nein'
574     u'T-Mobile (gut); A1' <=> u'T-Mobile (gut); A1'"""
575     def __init__(self):
576         NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList())))
577
578
579 class PhoneNumber(formencode.FancyValidator):
580     """Telefonnumber in international format, e.g. u'+43-699-1234567'"""
581     def __init__(self, default_cc=43):
582         self.validator = formencode.national.InternationalPhoneNumber(default_cc=lambda: default_cc)
583
584     def to_python(self, value):
585         return unicode(self.validator.to_python(value))
586
587     def from_python(self, value):
588         return self.validator.from_python(value)
589
590
591 class PhoneCommentListNeinLoopNone(NoneValidator):
592     """List with semicolon-separated phone numbers in international format with optional comment or 'Nein' as string:
593     u''                                                       <=> None
594     u'Nein'                                                   <=> u'Nein'
595     u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456' <=> u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456'
596     """
597     def __init__(self, comments_are_optional):
598         NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(PhoneNumber(default_cc=43), comments_are_optional=comments_are_optional))))
599
600
601 class MaskedEmail(formencode.FancyValidator):
602     """A masked email address as defined here is an email address that has the `@` character replacted by the text `(at)`.
603     So instead of `abd.def@example.com` it would be `abc.def(at)example.com`.
604     This validator takes either a normal or a masked email address in it's to_python method and returns the normal email address as well
605     as a bool indicating whether the email address was masked.
606     u''                       <=> (None, None)
607     u'abc.def@example.com'    <=> (u'abc.def@example.com', False)
608     u'abc.def(at)example.com' <=> (u'abc.def@example.com', True)
609     
610     """
611     def __init__(self, *args, **kw):
612         if not kw.has_key('strip'): kw['strip'] = True
613         if not kw.has_key('not_empty'): kw['not_empty'] = False
614         if not kw.has_key('if_empty'): kw['if_empty'] = (None, None)
615         self.at = '(at)'
616         formencode.FancyValidator.__init__(self, *args, **kw)
617
618     def _to_python(self, value, state):
619         email = value.replace(self.at, '@')
620         masked = value != email
621         val_email = formencode.validators.Email()
622         return val_email.to_python(email), masked
623
624     def _from_python(self, value, state):
625         email, masked = value
626         if email is None: return u''
627         val_email = formencode.validators.Email()
628         email = val_email.from_python(email)
629         if masked: email = email.replace('@', self.at)
630         return email
631
632
633 class EmailCommentListNeinLoopNone(NoneValidator):
634     """Converts a semicolon-separated list of email addresses with optional comments to itself.
635     The special value of u'Nein' indicates that there are no email addresses.
636     The empty string translates to None:
637     u''                                                   <=> None
638     u'Nein'                                               <=> u'Nein'
639     u'first@example.com'                                  <=> u'first@example.com'
640     u'first@example.com (Nur Winter); second@example.com' <=> u'first@example.com (Nur Winter); second@example.com'
641
642     If the parameter allow_masked_email is true, the following gives no error:
643     u'abc.def(at)example.com (comment)'                   <=> u'abc.def(at)example.com (comment)'
644     """
645     def __init__(self, allow_masked_email=False):
646         NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(MaskedEmail() if allow_masked_email else formencode.validators.Email()))))
647
648
649 class WikiPage(formencode.FancyValidator):
650     """Validates wiki page name like u'[[Birgitzer Alm]]'.
651     The page is not checked for existance.
652     An empty string is an error.
653     u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
654     """
655     def to_python(self, value):
656         self.assert_string(value, None)
657         if not value.startswith('[[') or not value.endswith(']]'): 
658             raise formencode.Invalid('No valid wiki page name', value, None)
659         return value
660     
661     def from_python(self, value):
662         return value
663
664
665 class WikiPageList(SemicolonList):
666     """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]'.
667     u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> [u'[[Birgitzer Alm]]', u'[[Kemater Alm]]']
668     u'[[Birgitzer Alm]]'                  <=> [u'[[Birgitzer Alm]]']
669     u''                                   <=> []
670     """
671     def __init__(self):
672         SemicolonList.__init__(self, WikiPage())
673
674
675 class WikiPageListLoopNone(NoneValidator):
676     """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]' as string.
677     u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> u'[[Birgitzer Alm]]; [[Kemater Alm]]'
678     u'[[Birgitzer Alm]]'                  <=> u'[[Birgitzer Alm]]'
679     u''                                   <=> None
680     """
681     def __init__(self):
682         NoneValidator.__init__(self, Loop(WikiPageList()))
683
684
685 class TupleSecondValidator(formencode.FancyValidator):
686     """Does not really validate anything but puts the string through
687     a validator in the second part of a tuple.
688     Examples with an Unsigned() validator and the True argument:
689     u'6' <=> (True, 6)
690     u'2' <=> (True, 2)"""
691     def __init__(self, first=True, validator=UnicodeNone()):
692         self.first = first
693         self.validator = validator
694     
695     def to_python(self, value):
696         self.assert_string(value, None)
697         return self.first, self.validator.to_python(value)
698     
699     def from_python(self, value):
700         assert value[0] == self.first
701         return self.validator.from_python(value[1])
702
703
704 class BoolUnicodeTupleValidator(NoneValidator):
705     """Translates an unparsed string or u'Nein' to a tuple:
706     u''         <=> (None, None)
707     u'Nein'     <=> (False, None)
708     u'any text' <=> (True, u'any text')
709     """
710     def __init__(self, validator=UnicodeNone()):
711         NoneValidator.__init__(self, NeinValidator(TupleSecondValidator(True, validator), (False, None)), (None, None))
712
713
714 class GermanLift(BoolUnicodeTupleValidator):
715     """Checks a lift_details property. It is a value comment property with the following
716     values allowed:
717     u'Sessellift'
718     u'Gondel'
719     u'Linienbus'
720     u'Taxi'
721     u'Sonstige'
722     Alternatively, the value u'Nein' is allowed.
723     An empty string maps to (None, None).
724     
725     Examples:
726     u''                                       <=> (None, None)
727     u'Nein'                                   <=> (False, None)
728     u'Sessellift                              <=> (True, u'Sessellift')
729     u'Gondel (nur bis zur Hälfte)'            <=> (True, u'Gondel (nur bis zur Hälfte)')
730     u'Sessellift; Taxi'                       <=> (True, u'Sessellift; Taxi')
731     u'Sessellift (Wochenende); Taxi (6 Euro)' <=> (True, u'Sessellift (Wochenende); Taxi (6 Euro)')
732     """
733     def __init__(self):
734         BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList(DictValidator({u'Sessellift': u'Sessellift', u'Gondel': u'Gondel', u'Linienbus': u'Linienbus', u'Taxi': u'Taxi', u'Sonstige': u'Sonstige'}))))
735         
736
737 class SledRental(BoolUnicodeTupleValidator):
738     """The value can be an empty string, u'Nein' or a comma-separated list of unicode strings with optional comments.
739     u''                                       <=> (None, None)
740     u'Nein'                                   <=> (False, None)
741     u'Talstation (nur mit Ticket); Schneealm' <=> (True, u'Talstation (nur mit Ticket); Schneealm')"""
742     def __init__(self):
743         BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList()))