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