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