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