518234556e7af52f8ade0c2b167fdc1a1548a681
[philipp/winterrodeln/wrpylib.git] / wrpylib / wrvalidators.py
1 #!/usr/bin/python3.4
2 # -*- coding: iso-8859-15 -*-
3 # $Id$
4 # $HeadURL$
5 """
6 A converter is a Python variable (may be a class, class instance or anything else) that has the member
7 functions from_str and to_str. From string takes a string "from the outside", checks it and returns a Python variable
8 representing that value in Python. It reports error by raising ValueError. to_str does the opposite, however, it
9 can assume that the value it has to convert to a string is valid. If it gets an invalid value, the behavior is
10 undefined.
11 """
12 import datetime
13 import urllib.parse
14 import re
15 import xml.dom.minidom as minidom
16 from xml.parsers.expat import ExpatError
17 from collections import OrderedDict, namedtuple
18
19 import mwparserfromhell
20 import formencode
21 import formencode.national
22 from wrpylib.mwmarkup import template_to_table
23
24
25 class OrderedSchema(formencode.Schema):
26     def _convert_to_python(self, value, state):
27         pre_validators = self.pre_validators
28         chained_validators = self.chained_validators
29         for validator in pre_validators:
30             value = validator.to_python(value, state)
31         self.pre_validators = []
32         self.chained_validators = []
33         try:
34             result = formencode.Schema._convert_to_python(self, value, state)
35             ordered_result = OrderedDict()
36             for key in value.keys():
37                 ordered_result[key] = result[key]
38             for validator in chained_validators:
39                 ordered_result = validator.to_python(ordered_result, state)
40         finally:
41             self.pre_validators = pre_validators
42             self.chained_validators = chained_validators
43         return ordered_result
44
45     def _convert_from_python(self, value, state):
46         # store original pre- and chained validators
47         pre_validators = self.pre_validators
48         chained_validators = self.chained_validators[:]
49         # apply chained validators
50         chained = chained_validators[:]
51         chained.reverse()
52         for validator in chained:
53             value = validator.from_python(value, state)
54         # tempoarly remove pre- and chained validators
55         self.pre_validators = []
56         self.chained_validators = []
57         # apply original _convert_from_python method
58         try:
59             result = formencode.Schema._convert_from_python(self, value, state)
60             ordered_result = OrderedDict()
61             for key in value.keys():
62                 ordered_result[key] = result[key]
63             # apply pre_validators
64             pre = pre_validators[:]
65             pre.reverse()
66             for validator in pre:
67                 ordered_result = validator.from_python(ordered_result, state)
68         finally:
69             # resore original pre- and chained_validators
70             self.pre_validators = pre_validators
71             self.chained_validators = chained_validators
72         return ordered_result
73
74
75 class NoneValidator(formencode.FancyValidator):
76     """Takes a validator and makes it possible that empty strings are mapped to None."""
77     def __init__(self, validator, python_none=None):
78         self.validator = validator
79         self.python_none = python_none
80     
81     def to_python(self, value, state=None):
82         self.assert_string(value, state)
83         if value == '': return self.python_none
84         return self.validator.to_python(value, state)
85     
86     def from_python(self, value, state=None):
87         if value == self.python_none: return ''
88         return self.validator.from_python(value, state)
89
90
91 class NeinValidator(formencode.FancyValidator):
92     """Take an arbitrary validator and adds the possibility that the
93     string can be u'Nein'.
94     Example together with an UnsignedNone validator:
95     >>> v = NeinValidator(UnsignedNone())
96     >>> v.to_python(u'')
97     None
98     >>> v.to_python(u'34')
99     34
100     >>> v.to_python(u'Nein')
101     u'Nein'
102     """
103     def __init__(self, validator, python_no='Nein'):
104         self.validator = validator
105         self.python_no = python_no
106     
107     def to_python(self, value, state=None):
108         self.assert_string(value, state)
109         if value == 'Nein': return self.python_no
110         return self.validator.to_python(value, state)
111     
112     def from_python(self, value, state=None):
113         if value == self.python_no: return 'Nein'
114         return self.validator.from_python(value, state)
115
116
117 class Unicode(formencode.FancyValidator):
118     """Converts an unicode string to an unicode string:
119     u'any string' <=> u'any string'"""
120     def to_python(self, value, state=None):
121         self.assert_string(value, state)
122         return str(value)
123
124     def from_python(self, value, state=None):
125         return str(value)
126
127
128 class UnicodeNone(NoneValidator):
129     """Converts an unicode string to an unicode string:
130     u'' <=> None
131     u'any string' <=> u'any string'"""
132     def __init__(self):
133         NoneValidator.__init__(self, Unicode())
134
135
136 class Unsigned(formencode.FancyValidator):
137     """Converts an unsigned number to a string and vice versa:
138     u'0'  <=>  0
139     u'1'  <=>  1
140     u'45' <=> 45
141     """
142     def __init__(self, max=None):
143         self.iv = formencode.validators.Int(min=0, max=max)
144
145     def to_python(self, value, state=None):
146         self.assert_string(value, state)
147         return self.iv.to_python(value, state)
148     
149     def from_python(self, value, state=None):
150         return str(value)
151
152
153 class UnsignedNone(NoneValidator):
154     """Converts an unsigned number to a string and vice versa:
155     u''   <=> None
156     u'0'  <=>  0
157     u'1'  <=>  1
158     u'45' <=> 45
159     """
160     def __init__(self, max=None):
161         NoneValidator.__init__(self, Unsigned(max))
162
163
164 class UnsignedNeinNone(NoneValidator):
165     """ Translates a number of Nein to a number.
166     u''     <=> None
167     u'Nein' <=> 0
168     u'1'    <=> 1
169     u'2'    <=> 2
170     ...
171     """
172     def __init__(self):
173         NoneValidator.__init__(self, UnsignedNone())
174
175
176 class Loop(formencode.FancyValidator):
177     """Takes a validator and calls from_python(to_python(value))."""
178     def __init__(self, validator):
179         self.validator = validator
180
181     def to_python(self, value, state=None):
182         self.assert_string(value, state)
183         return self.validator.from_python(self.validator.to_python(value, state))
184     
185     def from_python(self, value, state=None):
186         # we don't call self.validator.to_python(self.validator.from_python(value))
187         # here because our to_python implementation basically leaves the input untouched
188         # and so should from_python do.
189         return self.validator.from_python(self.validator.to_python(value, state))
190
191
192 class DictValidator(formencode.FancyValidator):
193     """Translates strings to other values via a python directory.
194     >>> boolValidator = DictValidator({u'': None, u'Ja': True, u'Nein': False})
195     >>> boolValidator.to_python(u'')
196     None
197     >>> boolValidator.to_python(u'Ja')
198     True
199     """
200     def __init__(self, dict):
201         self.dict = dict
202     
203     def to_python(self, value, state=None):
204         self.assert_string(value, state)
205         if value not in self.dict: raise formencode.Invalid("Key not found in dict.", value, state)
206         return self.dict[value]
207     
208     def from_python(self, value, state=None):
209         for k, v in self.dict.items():
210             if v == value:
211                 return k
212         raise formencode.Invalid('Invalid value', value, state)
213
214
215 class GermanBoolNone(DictValidator):
216     """Converts German bool values to the python bool type:
217     u''     <=> None
218     u'Ja'   <=> True
219     u'Nein' <=> False
220     """
221     def __init__(self):
222         DictValidator.__init__(self, {'': None, 'Ja': True, 'Nein': False})
223
224
225 class GermanTristateTuple(DictValidator):
226     """Does the following conversion:
227     u''          <=> (None, None)
228     u'Ja'        <=> (True, False)
229     u'Teilweise' <=> (True,  True)
230     u'Nein'      <=> (False, True)"""
231     def __init__(self, yes_python = (True, False), no_python = (False, True), partly_python = (True, True), none_python = (None, None)):
232         DictValidator.__init__(self, {'': none_python, 'Ja': yes_python, 'Nein': no_python, 'Teilweise': partly_python})
233
234
235 class GermanTristateFloat(GermanTristateTuple):
236     """Converts the a property with the possible values 0.0, 0.5, 1.0 or None
237     to a German text:
238     u''          <=> None
239     u'Ja'        <=> 1.0
240     u'Teilweise' <=> 0.5
241     u'Nein'      <=> 0.0"""
242     def __init__(self):
243         GermanTristateTuple.__init__(self, yes_python=1.0, no_python=0.0, partly_python=0.5, none_python=None)
244
245
246 class ValueComment(formencode.FancyValidator):
247     """Converts value with a potentially optional comment to a python tuple. If a comment is present, the
248     closing bracket has to be the rightmost character.
249     u''                                 <=> (None, None)
250     u'value'                            <=> (u'value', None)
251     u'value (comment)'                  <=> (u'value', u'comment')
252     u'[[link (linkcomment)]]'           <=> (u'[[link (linkcomment)]]', None)
253     u'[[link (linkcomment)]] (comment)' <=> (u'[[link (linkcomment)]]', comment)
254     """
255     def __init__(self, value_validator=UnicodeNone(), comment_validator=UnicodeNone(), comment_is_optional=True):
256         self.value_validator = value_validator
257         self.comment_validator = comment_validator
258         self.comment_is_optional = comment_is_optional
259     
260     def to_python(self, value, state=None):
261         self.assert_string(value, state)
262         if value == '':
263             v = value
264             c = value
265         else:
266             right = value.rfind(')')
267             if right+1 != len(value):
268                 if not self.comment_is_optional: raise formencode.Invalid('Mandatory comment not present', value, state)
269                 v = value
270                 c = ''
271             else:
272                 left = value.rfind('(')
273                 if left < 0: raise formencode.Invalid('Invalid format', value, state)
274                 v = value[:left].strip()
275                 c = value[left+1:right].strip()
276         return self.value_validator.to_python(v, state), self.comment_validator.to_python(c, state)
277
278     def from_python(self, value, state=None):
279         assert len(value) == 2
280         v = self.value_validator.from_python(value[0], state)
281         c = self.comment_validator.from_python(value[1], state)
282         if len(c) > 0:
283             if len(v) > 0: return '%s (%s)' % (v, c)
284             else: return '(%s)' % c
285         return v
286
287
288
289 class SemicolonList(formencode.FancyValidator):
290     """Applies a given validator to a semicolon separated list of values and returns a python list.
291     For an empty string an empty list is returned."""
292     def __init__(self, validator=Unicode()):
293         self.validator = validator
294     
295     def to_python(self, value, state=None):
296         self.assert_string(value, state)
297         return [self.validator.to_python(s.strip(), state) for s in value.split(';')]
298     
299     def from_python(self, value, state=None):
300         return "; ".join([self.validator.from_python(s, state) for s in value])
301         
302     
303 class ValueCommentList(SemicolonList):
304     """A value-comment list looks like one of the following lines:
305         value
306         value (optional comment)
307         value1; value2
308         value1; value2 (optional comment)
309         value1 (optional comment1); value2 (optional comment2); value3 (otional comment3)
310         value1 (optional comment1); value2 (optional comment2); value3 (otional comment3)
311     This function returns the value-comment list as list of tuples:
312         [(u'value1', u'comment1'), (u'value2', None)]
313     If no comment is present, None is specified.
314     For an empty string, [] is returned."""    
315     def __init__(self, value_validator=Unicode(), comments_are_optional=True):
316         SemicolonList.__init__(self, ValueComment(value_validator, comment_is_optional=comments_are_optional))
317
318
319 class GenericDateTime(formencode.FancyValidator):
320     """Converts a generic date/time information to a datetime class with a user defined format.
321     '2009-03-22 20:36:15' would be specified as '%Y-%m-%d %H:%M:%S'."""
322     
323     def __init__(self, date_time_format = '%Y-%m-%d %H:%M:%S', **keywords):
324         formencode.FancyValidator.__init__(self, **keywords)
325         self.date_time_format = date_time_format
326     
327     def to_python(self, value, state=None):
328         self.assert_string(value, state)
329         try: return datetime.datetime.strptime(value, self.date_time_format)
330         except ValueError as e: raise formencode.Invalid(str(e), value, state)
331     
332     def from_python(self, value, state=None):
333         return value.strftime(self.date_time_format)
334
335
336 class DateTimeNoSec(GenericDateTime):
337     def __init__(self, **keywords):
338         GenericDateTime.__init__(self, '%Y-%m-%d %H:%M', **keywords)
339
340
341 class DateNone(NoneValidator):
342     """Converts date information to date classes with the format '%Y-%m-%d' or None."""
343     def __init__(self):
344         NoneValidator.__init__(self, GenericDateTime('%Y-%m-%d'))
345
346
347
348 # Meta converter types and functions
349 # ----------------------------------
350
351 class Converter:
352     @classmethod
353     def from_str(cls, value):
354         return value
355
356     @classmethod
357     def to_str(cls, value):
358         return str(value)
359
360
361 FromToConverter = namedtuple('FromToConverter', ['from_str', 'to_str'])
362
363
364 def opt_from_str(value, from_str, none=None):
365     return none if value == '' else from_str(value)
366
367
368 def opt_to_str(value, to_str, none=None):
369     return '' if value == none else to_str(value)
370
371
372 class OptionalConverter(Converter):
373     converter = Converter
374     none = None
375
376     @classmethod
377     def from_str(cls, value):
378         return opt_from_str(value, cls.converter, cls.none)
379
380     @classmethod
381     def to_str(cls, value):
382         return opt_to_str(value, cls.converter, cls.none)
383
384
385 def choice_from_str(value, choices):
386     if value not in choices:
387         raise ValueError('{} is an invalid value')
388     return value
389
390
391 def dictkey_from_str(value, key_str_dict):
392     try:
393         return dict(list(zip(key_str_dict.values(), key_str_dict.keys())))[value]
394     except KeyError:
395         raise ValueError("Invalid value '{}'".format(value))
396
397
398 def dictkey_to_str(value, key_str_dict):
399     try:
400         return key_str_dict[value]
401     except KeyError:
402         raise ValueError("Invalid value '{}'".format(value))
403
404
405 class DictKeyConverter(Converter):
406     key_str_dict = OrderedDict()
407
408     @classmethod
409     def from_str(cls, value):
410         return dictkey_from_str(value, cls.key_str_dict)
411
412     @classmethod
413     def to_str(cls, value):
414         return dictkey_to_str(value, cls.key_str_dict)
415
416
417
418 # Basic type converter functions
419 # ------------------------------
420
421
422 def str_from_str(value):
423     return value
424
425
426 def str_to_str(value):
427     return value
428
429
430 def opt_str_from_str(value):
431     return opt_from_str(value, str_from_str)
432
433
434 def opt_str_to_str(value):
435     return opt_to_str(value, str_to_str)
436
437
438 opt_str_converter = FromToConverter(opt_str_from_str, opt_str_to_str)
439
440
441 def req_str_from_str(value):
442     if value == '':
443         raise ValueError('missing required value')
444     return str_from_str(value)
445
446
447 class Str(Converter):
448     pass
449
450
451 class OptStr(OptionalConverter):
452     converter = Str
453
454
455 def int_from_str(value, min=None, max=None):
456     value = int(value)
457     if min is not None and value < min:
458         raise ValueError('{} must be >= than {}'.format(value, min))
459     if max is not None and value > max:
460         raise ValueError('{} must be <= than {}'.format(value, max))
461     return value
462
463
464 def int_to_str(value):
465     return str(value)
466
467
468 def opt_int_from_str(value, min=None, max=None):
469     return opt_from_str(value, lambda val: int_from_str(val, min, max))
470
471
472 def opt_int_to_str(value):
473     return opt_to_str(value, int_to_str)
474
475
476 opt_int_converter = FromToConverter(opt_int_from_str, opt_int_to_str)
477
478
479 class Int(Converter):
480     min = None
481     max = None
482
483     @classmethod
484     def from_str(cls, value):
485         return int_from_str(value, cls.min, cls.max)
486
487
488 IntConverter = FromToConverter(int_from_str, int_to_str)
489
490
491 class OptInt(OptionalConverter):
492     converter = Int
493
494
495 class DateTime(Converter):
496     format='%Y-%m-%d %H:%M:%S'
497
498     @classmethod
499     def from_str(cls, value):
500         return datetime.datetime.strptime(value, cls.format)
501
502     @classmethod
503     def to_str(cls, value):
504         return value.strftime(cls.format)
505
506
507 # Complex types
508 # -------------
509
510 def enum_from_str(value, from_str=req_str_from_str, separator=';', min_len=0):
511     """Semicolon separated list of entries with the same "type"."""
512     values = value.split(separator)
513     if len(values) == 1 and values[0] == '':
514         values = []
515     if len(values) < min_len:
516         raise ValueError('at least {} entry/entries have to be in the enumeration'.format(min_len))
517     return list(map(from_str, map(str.strip, values)))
518
519
520 def enum_to_str(value, to_str=opt_str_to_str, separator='; '):
521     return separator.join(map(to_str, value))
522
523
524 # Specific converter functions
525 # ----------------------------
526
527 BOOL_GERMAN = OrderedDict([(False, 'Nein'), (True, 'Ja')])
528
529
530 def bool_german_from_str(value):
531     return dictkey_from_str(value, BOOL_GERMAN)
532
533
534 def bool_german_to_str(value):
535     return dictkey_to_str(value, BOOL_GERMAN)
536
537
538 def opt_bool_german_from_str(value):
539     return opt_from_str(value, bool_german_from_str)
540
541
542 def opt_bool_german_to_str(value):
543     return opt_to_str(value, bool_german_to_str)
544
545
546 opt_bool_german_converter = FromToConverter(opt_bool_german_from_str, opt_bool_german_to_str)
547
548
549 TRISTATE_GERMAN = OrderedDict([(0.0, 'Nein'), (0.5, 'Teilweise'), (1.0, 'Ja')])
550
551
552 def tristate_german_from_str(value):
553     return dictkey_from_str(value, TRISTATE_GERMAN)
554
555
556 def tristate_german_to_str(value):
557     return dictkey_to_str(value, TRISTATE_GERMAN)
558
559
560 def opt_tristate_german_from_str(value):
561     return opt_from_str(value, tristate_german_from_str)
562
563
564 def opt_tristate_german_to_str(value):
565     return opt_to_str(value, tristate_german_to_str)
566
567
568 def meter_from_str(value):
569     return int_from_str(value, min=0)
570
571
572 def meter_to_str(value):
573     return int_to_str(value)
574
575
576 def opt_meter_from_str(value):
577     return opt_from_str(value, meter_from_str)
578
579
580 def opt_meter_to_str(value):
581     return opt_to_str(value, meter_to_str)
582
583
584 opt_meter_converter = FromToConverter(opt_meter_from_str, opt_meter_to_str)
585
586
587 def minutes_from_str(value):
588     return int_from_str(value, min=0)
589
590
591 def minutes_to_str(value):
592     return int_to_str(value)
593
594
595 def opt_minutes_from_str(value):
596     return opt_from_str(value, minutes_from_str)
597
598
599 def opt_minutes_to_str(value):
600     return opt_to_str(value, minutes_to_str)
601
602
603 opt_minutes_converter = FromToConverter(opt_minutes_from_str, opt_minutes_to_str)
604
605
606 LonLat = namedtuple('LonLat', ['lon', 'lat'])
607
608
609 lonlat_none = LonLat(None, None)
610
611
612 def lonlat_from_str(value):
613     """Converts a winterrodeln geo string like '47.076207 N 11.453553 E' (being '<latitude> N <longitude> E'
614     to the LonLat(lon, lat) named  tupel."""
615     r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value)
616     if r is None: raise ValueError("Coordinates '{}' have not a format like '47.076207 N 11.453553 E'".format(value))
617     return LonLat(float(r.groups()[1]), float(r.groups()[0]))
618
619
620 def lonlat_to_str(value):
621     return '{:.6f} N {:.6f} E'.format(value.lat, value.lon)
622
623
624 def opt_lonlat_from_str(value):
625     return opt_from_str(value, lonlat_from_str, lonlat_none)
626
627
628 def opt_lonlat_to_str(value):
629     return opt_to_str(value, lonlat_to_str, lonlat_none)
630
631
632 opt_lonlat_converter = FromToConverter(opt_lonlat_from_str, opt_lonlat_to_str)
633
634
635
636 class MultiGeo(formencode.FancyValidator):
637     "Formats multiple coordinates, even in multiple lines to [(latitude, longitude, elevation), ...] or [(latitude, longitude, None), ...] tuplets."
638     
639     # Valid for input_format
640     FORMAT_GUESS = 0         # guesses the input format; default for input_format
641     FORMAT_NONE = -1          # indicates missing formats
642     
643     # Valid for input_format and output_format
644     FORMAT_GEOCACHING = 1    # e.g. "N 47° 13.692 E 011° 25.535"
645     FORMAT_WINTERRODELN = 2  # e.g. "47.222134 N 11.467211 E"
646     FORMAT_GMAPPLUGIN = 3    # e.g. "47.232922, 11.452239"
647     FORMAT_GPX = 4           # e.g. "<trkpt lat="47.181289" lon="11.408827"><ele>1090.57</ele></trkpt>"
648     
649     input_format = FORMAT_GUESS
650     output_format = FORMAT_WINTERRODELN
651     last_input_format = FORMAT_NONE
652
653     def __init__(self, input_format = FORMAT_GUESS, output_format = FORMAT_WINTERRODELN, **keywords):
654         self.input_format = input_format
655         self.output_format = output_format
656         formencode.FancyValidator.__init__(self, if_empty = (None, None, None), **keywords)
657     
658     def to_python(self, value, state=None):
659         self.assert_string(value, state)
660         input_format = self.input_format
661         if not input_format in [self.FORMAT_GUESS, self.FORMAT_GEOCACHING, self.FORMAT_WINTERRODELN, self.FORMAT_GMAPPLUGIN, self.FORMAT_GPX]:
662             raise formencode.Invalid("input_format %d is not recognized" % input_format, value, state) # Shouldn't it be an other type of runtime error?
663         lines = [line.strip() for line in value.split("\n") if len(line.strip()) > 0]
664         
665         result = []
666         for line in lines:
667             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GEOCACHING:
668                 r = re.match('N ?(\d+)° ?(\d+\.\d+) +E ?(\d+)° ?(\d+\.\d+)', line)
669                 if not r is None:
670                     g = r.groups()
671                     result.append((float(g[0]) + float(g[1])/60, float(g[2]) + float(g[3])/60, None))
672                     last_input_format = self.FORMAT_WINTERRODELN
673                     continue
674                     
675             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_WINTERRODELN:
676                 r = re.match('(\d+\.\d+) N (\d+\.\d+) E', line)
677                 if not r is None:
678                     result.append((float(r.groups()[0]), float(r.groups()[1]), None))
679                     last_input_format = self.FORMAT_WINTERRODELN
680                     continue
681                 
682             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GMAPPLUGIN:
683                 r = re.match('(\d+\.\d+), ?(\d+\.\d+)', line)
684                 if not r is None:
685                     result.append((float(r.groups()[0]), float(r.groups()[1]), None))
686                     last_input_format = self.FORMAT_GMAPPLUGIN
687                     continue
688                 
689             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GPX:
690                 try:
691                     xml = minidom.parseString(line)
692                     coord = xml.documentElement
693                     lat = float(coord.getAttribute('lat'))
694                     lon = float(coord.getAttribute('lon'))
695                     try: ele = float(coord.childNodes[0].childNodes[0].nodeValue)
696                     except (IndexError, ValueError): ele = None
697                     result.append((lat, lon, ele))
698                     last_input_format = self.FORMAT_GPX
699                     continue
700                 except (ExpatError, IndexError, ValueError): pass
701
702             raise formencode.Invalid("Coordinates '%s' have no known format" % line, value, state)
703             
704         return result
705     
706     def from_python(self, value, state=None):
707         output_format = self.output_format
708         result = []
709         for latitude, longitude, height in value:
710             if output_format == self.FORMAT_GEOCACHING:
711                 degree = latitude
712                 result.append('N %02d° %02.3f E %03d° %02.3f' % (latitude, latitude % 1 * 60, longitude, longitude % 1 * 60))
713                 
714             elif output_format == self.FORMAT_WINTERRODELN:
715                 result.append('%.6f N %.6f E' % (latitude, longitude))
716
717             elif output_format == self.FORMAT_GMAPPLUGIN:
718                 result.append('%.6f, %.6f' % (latitude, longitude))
719                 
720             elif output_format == self.FORMAT_GPX:
721                 if not height is None: result.append('<trkpt lat="%.6f" lon="%.6f"><ele>%.2f</ele></trkpt>' % (latitude, longitude, height))
722                 else: result.append('<trkpt lat="%.6f" lon="%.6f"/>' % (latitude, longitude))
723             
724             else:
725                 raise formencode.Invalid("output_format %d is not recognized" % output_format, value, state) # Shouldn't it be an other type of runtime error?
726             
727         return "\n".join(result)
728
729
730 DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')])
731
732
733 def difficulty_german_from_str(value):
734     return dictkey_from_str(value, DIFFICULTY_GERMAN)
735
736
737 def difficulty_german_to_str(value):
738     return dictkey_to_str(value, DIFFICULTY_GERMAN)
739
740
741 def opt_difficulty_german_from_str(value):
742     return opt_from_str(value, difficulty_german_from_str)
743
744
745 def opt_difficulty_german_to_str(value):
746     return opt_to_str(value, difficulty_german_to_str)
747
748
749 opt_difficulty_german_converter = FromToConverter(opt_difficulty_german_from_str, opt_difficulty_german_to_str)
750
751
752 AVALANCHES_GERMAN = OrderedDict([(1, 'kaum'), (2, 'selten'), (3, 'gelegentlich'), (4, 'häufig')])
753
754
755 def avalanches_german_from_str(value):
756     return dictkey_from_str(value, AVALANCHES_GERMAN)
757
758
759 def avalanches_german_to_str(value):
760     return dictkey_to_str(value, AVALANCHES_GERMAN)
761
762
763 def opt_avalanches_german_from_str(value):
764     return opt_from_str(value, avalanches_german_from_str)
765
766
767 def opt_avalanches_german_to_str(value):
768     return opt_to_str(value, avalanches_german_to_str)
769
770
771 opt_avalanches_german_converter = FromToConverter(opt_avalanches_german_from_str, opt_avalanches_german_to_str)
772
773
774 PUBLIC_TRANSPORT_GERMAN = OrderedDict([(1, 'Sehr gut'), (2, 'Gut'), (3, 'Mittelmäßig'), (4, 'Schlecht'), (5, 'Nein'), (6, 'Ja')])
775
776
777 def public_transport_german_from_str(value):
778     return dictkey_from_str(value, PUBLIC_TRANSPORT_GERMAN)
779
780
781 def public_transport_german_to_str(value):
782     return dictkey_to_str(value, PUBLIC_TRANSPORT_GERMAN)
783
784
785 def opt_public_transport_german_from_str(value):
786     return opt_from_str(value, public_transport_german_from_str)
787
788
789 def opt_public_transport_german_to_str(value):
790     return opt_to_str(value, public_transport_german_to_str)
791
792
793 opt_public_transport_german_converter = FromToConverter(opt_public_transport_german_from_str, opt_public_transport_german_to_str)
794
795
796 def value_comment_from_str(value, value_from_str=str_from_str, comment_from_str=str_from_str, comment_optional=False):
797     """Makes it possible to have a mandatory comment in parenthesis at the end of the string."""
798     open_brackets = 0
799     comment = ''
800     comment_end_pos = None
801     for i, char in enumerate(value[::-1]):
802         if char == ')':
803             open_brackets += 1
804             if open_brackets == 1:
805                 comment_end_pos = i
806                 if len(value[-1-comment_end_pos:].rstrip()) > 1:
807                     raise ValueError('invalid characters after comment')
808         elif char == '(':
809             open_brackets -= 1
810             if open_brackets == 0:
811                 comment = value[-i:-1-comment_end_pos]
812                 value = value[:-i-1].rstrip()
813                 break
814     else:
815         if open_brackets > 0:
816             raise ValueError('bracket mismatch')
817         if not comment_optional:
818             raise ValueError('mandatory comment not found')
819     return value_from_str(value), comment_from_str(comment)
820
821
822 def value_comment_to_str(value, value_to_str=str_to_str, comment_to_str=str_to_str, comment_optional=False):
823     left = value_to_str(value[0])
824     comment = comment_to_str(value[1])
825     if len(comment) > 0 or not comment_optional:
826         comment = '({})'.format(comment)
827     if len(left) == 0:
828         return comment
829     if len(comment) == 0:
830         return left
831     return '{} {}'.format(left, comment)
832
833
834 def opt_tristate_german_comment_from_str(value):
835     """Ja, Nein or Vielleicht, optionally with comment in parenthesis."""
836     return value_comment_from_str(value, opt_tristate_german_from_str, opt_str_from_str, True)
837
838
839 def opt_tristate_german_comment_to_str(value):
840     return value_comment_to_str(value, opt_tristate_german_to_str, opt_str_to_str, True)
841
842
843 opt_tristate_german_comment_converter = FromToConverter(opt_tristate_german_comment_from_str, opt_tristate_german_comment_to_str)
844
845
846 def no_german_from_str(value, from_str=req_str_from_str, use_tuple=True, no_value=None):
847     if value == 'Nein':
848         return (False, no_value) if use_tuple else no_value
849     return (True, from_str(value)) if use_tuple else from_str(value)
850
851
852 def no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None):
853     if use_tuple:
854         if not value[0]:
855             return 'Nein'
856         return to_str(value[1])
857     else:
858         if value == no_value:
859             return 'Nein'
860         return to_str(value)
861
862
863 def opt_no_german_from_str(value, from_str=str_from_str, use_tuple=True, no_value=None, none=(None, None)):
864     return opt_from_str(value, lambda v: no_german_from_str(v, from_str, use_tuple, no_value), none)
865
866
867 def opt_no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None, none=(None, None)):
868     return opt_to_str(value, lambda v: no_german_to_str(v, to_str, use_tuple, no_value), none)
869
870
871 class GermanTristateFloatComment(ValueComment):
872     """Converts the a property with the possible values 0.0, 0.5, 1.0 or None and an optional comment
873     in parenthesis to a German text:
874     u''                  <=> (None, None)
875     u'Ja'                <=> (1.0,  None)
876     u'Teilweise'         <=> (0.5,  None)
877     u'Nein'              <=> (0.0,  None)
878     u'Ja (aber schmal)'  <=> (1.0,  u'aber schmal')
879     u'Teilweise (oben)'  <=> (0.5,  u'oben')
880     u'Nein (aber breit)' <=> (0.0,  u'aber breit')
881     """
882     def __init__(self):
883         ValueComment.__init__(self, GermanTristateFloat())
884
885
886 def night_light_from_str(value):
887     """'Beleuchtungsanlage' Tristate with optional comment:
888     ''                  <=> (None, None)
889     'Ja'                <=> (1.0,  None)
890     'Teilweise'         <=> (0.5,  None)
891     'Nein'              <=> (0.0,  None)
892     'Ja (aber schmal)'  <=> (1.0,  'aber schmal')
893     'Teilweise (oben)'  <=> (0.5,  'oben')
894     'Nein (aber breit)' <=> (0.0,  'aber breit')
895     """
896     return
897
898
899 class NightLightDays(Int):
900     min = 0
901     max = 7
902
903
904 class OptNightLightDays(OptionalConverter):
905     converter = NightLightDays
906
907
908 def nightlightdays_from_str(value):
909     return value_comment_from_str(value, lambda val: opt_from_str(val, lambda v: int_from_str(v, min=0, max=7)), opt_str_from_str, comment_optional=True)
910
911
912 def nightlightdays_to_str(value):
913     return value_comment_to_str(value, lambda val: opt_to_str(val, int_to_str), opt_str_to_str, comment_optional=True)
914
915
916 nightlightdays_converter = FromToConverter(nightlightdays_from_str, nightlightdays_to_str)
917
918
919 class UnsignedCommentNone(NoneValidator):
920     """Converts the a property with unsigned values an optional comment
921     in parenthesis to a text:
922     u''           <=> (None, None)
923     u'2 (Mo, Di)' <=> (2,  u'Mo, Di')
924     u'7'          <=> (7,  None)
925     u'0'          <=> (0,  None)
926     """
927     def __init__(self, max=None):
928         NoneValidator.__init__(self, ValueComment(Unsigned(max=max)), (None, None))
929
930
931 CACHET_REGEXP = [r'(Tiroler Naturrodelbahn-Gütesiegel) ([12]\d{3}) (leicht|mittel|schwer)$']
932
933
934 def single_cachet_german_from_str(value):
935     for pattern in CACHET_REGEXP:
936         match = re.match(pattern, value)
937         if match:
938             return match.groups()
939     raise ValueError("'{}' is no valid cachet".format(value))
940
941
942 def single_cachet_german_to_str(value):
943     return ' '.join(value)
944
945
946 def cachet_german_from_str(value):
947     """Converts a "Gütesiegel":
948     '' => None
949     'Nein' => []
950     'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' => [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]"""
951     return opt_no_german_from_str(value, lambda val: enum_from_str(val, single_cachet_german_from_str), False, [], None)
952
953     
954 def cachet_german_to_str(value):
955     return opt_no_german_to_str(value, lambda val: enum_to_str(val, single_cachet_german_to_str), False, [], None)
956
957
958 cachet_german_converter = FromToConverter(cachet_german_from_str, cachet_german_to_str)
959
960
961 def url_from_str(value):
962     result = urllib.parse.urlparse(value)
963     if result.scheme not in ['http', 'https']:
964         raise ValueError('scheme has to be http or https')
965     if not result.netloc:
966         raise ValueError('url does not contain netloc')
967     return value
968
969
970 def url_to_str(value):
971     return value
972
973
974 def webauskunft_from_str(value):
975     return opt_no_german_from_str(value, url_from_str)
976
977
978 def webauskunft_to_str(value):
979     return opt_no_german_to_str(value, url_to_str)
980
981
982 webauskunft_converter = FromToConverter(webauskunft_from_str, webauskunft_to_str)
983
984
985 class Url(formencode.FancyValidator):
986     """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed."""
987     # formencode 1.2.5 to formencode 1.3.0a1 sometimes raise ValueError instead of Invalid exceptions
988     # https://github.com/formencode/formencode/pull/61
989     urlv = formencode.validators.URL()    
990
991     def to_python(self, value, state=None):
992         self.assert_string(value, state)
993         v = value
994         v = v.replace('ä', 'a')
995         v = v.replace('ö', 'o')
996         v = v.replace('ü', 'u')
997         v = v.replace('ß', 'ss')
998         v = self.urlv.to_python(v, state)
999         return value
1000     
1001     def from_python(self, value, state=None):
1002         return value
1003
1004
1005 class UrlNeinNone(NoneValidator):
1006     """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed.
1007     The special value u"Nein" is allowed."""
1008     def __init__(self):
1009         NoneValidator.__init__(self, NeinValidator(Url()))
1010
1011
1012 class ValueCommentListNeinLoopNone(NoneValidator):
1013     """Translates a semicolon separated list of values with optional comments in paranthesis or u'Nein' to itself.
1014     An empty string is translated to None:
1015     u''                   <=> None
1016     u'Nein'               <=> u'Nein'
1017     u'T-Mobile (gut); A1' <=> u'T-Mobile (gut); A1'"""
1018     def __init__(self):
1019         NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList())))
1020
1021
1022 def phone_number_from_str(value):
1023     match = re.match(r'\+\d+(-\d+)*$', value)
1024     if match is None:
1025         raise ValueError('invalid format of phone number - use something like +43-699-1234567')
1026     return value
1027
1028
1029 def phone_number_to_str(value):
1030     return value
1031
1032
1033 def telefonauskunft_from_str(value):
1034     return opt_no_german_from_str(value, lambda val: enum_from_str(val, lambda v: value_comment_from_str(v, phone_number_from_str, req_str_from_str, False)), False, [], None)
1035
1036
1037 def telefonauskunft_to_str(value):
1038     return opt_no_german_to_str(value, lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, phone_number_to_str, str_to_str)), False, [], None)
1039
1040
1041 telefonauskunft_converter = FromToConverter(telefonauskunft_from_str, telefonauskunft_to_str)
1042
1043
1044 class PhoneNumber(formencode.FancyValidator):
1045     """Telefonnumber in international format, e.g. u'+43-699-1234567'"""
1046     def __init__(self, default_cc=43):
1047         self.validator = formencode.national.InternationalPhoneNumber(default_cc=lambda: default_cc)
1048
1049     def to_python(self, value, state=None):
1050         return str(self.validator.to_python(value, state))
1051
1052     def from_python(self, value, state=None):
1053         return self.validator.from_python(value, state)
1054
1055
1056 class PhoneCommentListNeinLoopNone(NoneValidator):
1057     """List with semicolon-separated phone numbers in international format with optional comment or 'Nein' as string:
1058     u''                                                       <=> None
1059     u'Nein'                                                   <=> u'Nein'
1060     u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456' <=> u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456'
1061     """
1062     def __init__(self, comments_are_optional):
1063         NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(PhoneNumber(default_cc=43), comments_are_optional=comments_are_optional))))
1064
1065
1066 class MaskedEmail(formencode.FancyValidator):
1067     """A masked email address as defined here is an email address that has the `@` character replacted by the text `(at)`.
1068     So instead of `abd.def@example.com` it would be `abc.def(at)example.com`.
1069     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
1070     as a bool indicating whether the email address was masked.
1071     u''                       <=> (None, None)
1072     u'abc.def@example.com'    <=> (u'abc.def@example.com', False)
1073     u'abc.def(at)example.com' <=> (u'abc.def@example.com', True)
1074     
1075     """
1076     def __init__(self, *args, **kw):
1077         if 'strip' not in kw: kw['strip'] = True
1078         if 'not_empty' not in kw: kw['not_empty'] = False
1079         if 'if_empty' not in kw: kw['if_empty'] = (None, None)
1080         self.at = '(at)'
1081         formencode.FancyValidator.__init__(self, *args, **kw)
1082
1083     def _to_python(self, value, state=None):
1084         email = value.replace(self.at, '@')
1085         masked = value != email
1086         val_email = formencode.validators.Email()
1087         return val_email.to_python(email, state), masked
1088
1089     def _from_python(self, value, state=None):
1090         email, masked = value
1091         if email is None: return ''
1092         val_email = formencode.validators.Email()
1093         email = val_email.from_python(email, state)
1094         if masked: email = email.replace('@', self.at)
1095         return email
1096
1097
1098 class EmailCommentListNeinLoopNone(NoneValidator):
1099     """Converts a semicolon-separated list of email addresses with optional comments to itself.
1100     The special value of u'Nein' indicates that there are no email addresses.
1101     The empty string translates to None:
1102     u''                                                   <=> None
1103     u'Nein'                                               <=> u'Nein'
1104     u'first@example.com'                                  <=> u'first@example.com'
1105     u'first@example.com (Nur Winter); second@example.com' <=> u'first@example.com (Nur Winter); second@example.com'
1106
1107     If the parameter allow_masked_email is true, the following gives no error:
1108     u'abc.def(at)example.com (comment)'                   <=> u'abc.def(at)example.com (comment)'
1109     """
1110     def __init__(self, allow_masked_email=False):
1111         NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(MaskedEmail() if allow_masked_email else formencode.validators.Email()))))
1112
1113
1114 class WikiPage(formencode.FancyValidator):
1115     """Validates wiki page name like u'[[Birgitzer Alm]]'.
1116     The page is not checked for existance.
1117     An empty string is an error.
1118     u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
1119     """
1120     def to_python(self, value, state=None):
1121         self.assert_string(value, state)
1122         if not value.startswith('[[') or not value.endswith(']]'): 
1123             raise formencode.Invalid('No valid wiki page name', value, state)
1124         return value
1125     
1126     def from_python(self, value, state=None):
1127         return value
1128
1129
1130 class WikiPageList(SemicolonList):
1131     """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]'.
1132     u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> [u'[[Birgitzer Alm]]', u'[[Kemater Alm]]']
1133     u'[[Birgitzer Alm]]'                  <=> [u'[[Birgitzer Alm]]']
1134     u''                                   <=> []
1135     """
1136     def __init__(self):
1137         SemicolonList.__init__(self, WikiPage())
1138
1139
1140 class WikiPageListLoopNone(NoneValidator):
1141     """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]' as string.
1142     u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> u'[[Birgitzer Alm]]; [[Kemater Alm]]'
1143     u'[[Birgitzer Alm]]'                  <=> u'[[Birgitzer Alm]]'
1144     u''                                   <=> None
1145     """
1146     def __init__(self):
1147         NoneValidator.__init__(self, Loop(WikiPageList()))
1148
1149
1150 class TupleSecondValidator(formencode.FancyValidator):
1151     """Does not really validate anything but puts the string through
1152     a validator in the second part of a tuple.
1153     Examples with an Unsigned() validator and the True argument:
1154     u'6' <=> (True, 6)
1155     u'2' <=> (True, 2)"""
1156     def __init__(self, first=True, validator=UnicodeNone()):
1157         self.first = first
1158         self.validator = validator
1159     
1160     def to_python(self, value, state=None):
1161         self.assert_string(value, state)
1162         return self.first, self.validator.to_python(value, state)
1163     
1164     def from_python(self, value, state=None):
1165         assert value[0] == self.first
1166         return self.validator.from_python(value[1], state)
1167
1168
1169 class BoolUnicodeTupleValidator(NoneValidator):
1170     """Translates an unparsed string or u'Nein' to a tuple:
1171     ''         <=> (None, None)
1172     'Nein'     <=> (False, None)
1173     'any text' <=> (True, 'any text')
1174     """
1175     def __init__(self, validator=UnicodeNone()):
1176         NoneValidator.__init__(self, NeinValidator(TupleSecondValidator(True, validator), (False, None)), (None, None))
1177
1178
1179 LIFT_GERMAN = ['Sessellift', 'Gondel', 'Linienbus', 'Taxi', 'Sonstige']
1180
1181
1182 def lift_german_from_str(value):
1183     """Checks a lift_details property. It is a value comment property with the following
1184     values allowed:
1185     'Sessellift'
1186     'Gondel'
1187     'Linienbus'
1188     'Taxi'
1189     'Sonstige'
1190     Alternatively, the value u'Nein' is allowed.
1191     An empty string maps to (None, None).
1192
1193     Examples:
1194     ''                                       <=> None
1195     'Nein'                                   <=> []
1196     'Sessellift                              <=> [('Sessellift', None)]
1197     'Gondel (nur bis zur Hälfte)'            <=> [('Gondel', 'nur bis zur Hälfte')]
1198     'Sessellift; Taxi'                       <=> [('Sessellift', None), ('Taxi', None)]
1199     'Sessellift (Wochenende); Taxi (6 Euro)' <=> [('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')]
1200     """
1201     return opt_no_german_from_str(value, lambda value_enum: enum_from_str(value_enum, lambda value_comment: value_comment_from_str(value_comment, lambda v: choice_from_str(v, LIFT_GERMAN), opt_str_from_str, comment_optional=True)), use_tuple=False, no_value=[], none=None)
1202
1203
1204 def lift_german_to_str(value):
1205     return opt_no_german_to_str(value, lambda value_enum: enum_to_str(value_enum, lambda value_comment: value_comment_to_str(value_comment, str_to_str, opt_str_to_str, comment_optional=True)), use_tuple=False, no_value=[], none=None)
1206
1207
1208 lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str)
1209
1210
1211 class GermanLift(BoolUnicodeTupleValidator):
1212     """Checks a lift_details property. It is a value comment property with the following
1213     values allowed:
1214     u'Sessellift'
1215     u'Gondel'
1216     u'Linienbus'
1217     u'Taxi'
1218     u'Sonstige'
1219     Alternatively, the value u'Nein' is allowed.
1220     An empty string maps to (None, None).
1221     
1222     Examples:
1223     u''                                       <=> (None, None)
1224     u'Nein'                                   <=> (False, None)
1225     u'Sessellift                              <=> (True, u'Sessellift')
1226     u'Gondel (nur bis zur Hälfte)'            <=> (True, u'Gondel (nur bis zur Hälfte)')
1227     u'Sessellift; Taxi'                       <=> (True, u'Sessellift; Taxi')
1228     u'Sessellift (Wochenende); Taxi (6 Euro)' <=> (True, u'Sessellift (Wochenende); Taxi (6 Euro)')
1229     """
1230     def __init__(self):
1231         BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList(DictValidator({'Sessellift': 'Sessellift', 'Gondel': 'Gondel', 'Linienbus': 'Linienbus', 'Taxi': 'Taxi', 'Sonstige': 'Sonstige'}))))
1232         
1233
1234 def sledrental_from_str(value):
1235     """The value can be an empty string, 'Nein' or a semicolon-separated list of strings with optional comments.
1236     ''                                       => None
1237     'Nein'                                   => []
1238     'Talstation (nur mit Ticket); Schneealm' => [('Talstation', 'nur mit Ticket'), ('Schneealm', None)]"""
1239     return opt_no_german_from_str(value, lambda val: enum_from_str(val, lambda v: value_comment_from_str(v, req_str_from_str, opt_str_from_str, True)), False, [], None)
1240
1241
1242 def sledrental_to_str(value):
1243     return opt_no_german_to_str(value, lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, str_to_str, opt_str_to_str, True)), False, [], None)
1244
1245
1246 sledrental_converter = FromToConverter(sledrental_from_str, sledrental_to_str)
1247
1248
1249 class ValueErrorList(ValueError):
1250     pass
1251
1252
1253 def box_from_template(template, name, converter_dict):
1254     if template.name.strip() != name:
1255         raise ValueError('Box name has to be "{}"'.format(name))
1256     result = OrderedDict()
1257     exceptions_dict = OrderedDict()
1258     # check values
1259     for key, converter in converter_dict.items():
1260         try:
1261             if not template.has(key):
1262                 raise ValueError('Missing parameter "{}"'.format(key))
1263             result[key] = converter.from_str(str(template.get(key).value.strip()))
1264         except ValueError as e:
1265             exceptions_dict[key] = e
1266     # check if keys are superfluous
1267     superfluous_keys = {str(p.name.strip()) for p in template.params} - set(converter_dict.keys())
1268     for key in superfluous_keys:
1269         exceptions_dict[key] = ValueError('Superfluous parameter: "{}"'.format(key))
1270     if len(exceptions_dict) > 0:
1271         raise ValueErrorList('{} error(s) occurred when parsing template parameters.'.format(len(exceptions_dict)), exceptions_dict)
1272     return result
1273
1274
1275 def box_to_template(value, name, converter_dict):
1276     template = mwparserfromhell.nodes.template.Template(name)
1277     for key, converter in converter_dict.items():
1278         template.add(key, converter.to_str(value[key]))
1279     return template
1280
1281
1282 def template_from_str(value, name):
1283     wikicode = mwparserfromhell.parse(value)
1284     template_list = wikicode.filter_templates(name)
1285     if len(name) == 0:
1286         raise ValueError('No "{}" template was found'.format(name))
1287     if len(template_list) > 1:
1288         raise ValueError('{} "{}" templates were found'.format(len(template_list), name))
1289     return template_list[0]
1290
1291
1292 def box_from_str(value, name, converter_dict):
1293     template = template_from_str(value, name)
1294     return box_from_template(template, name, converter_dict)
1295
1296
1297 def box_to_str(value, name, converter_dict):
1298     return str(box_to_template(value, name, converter_dict))
1299
1300
1301 RODELBAHNBOX_TEMPLATE_NAME = 'Rodelbahnbox'
1302
1303
1304 RODELBAHNBOX_DICT = OrderedDict([
1305     ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
1306     ('Position oben', opt_lonlat_converter), # '47.583333 N 15.75 E'
1307     ('Höhe oben', opt_meter_converter), # '2000'
1308     ('Position unten', opt_lonlat_converter), # '47.583333 N 15.75 E'
1309     ('Höhe unten', opt_meter_converter), # '1200'
1310     ('Länge', opt_meter_converter), # 3500
1311     ('Schwierigkeit', opt_difficulty_german_converter), # 'mittel'
1312     ('Lawinen', opt_avalanches_german_converter), # 'kaum'
1313     ('Betreiber', opt_str_converter), # 'Max Mustermann'
1314     ('Öffentliche Anreise', opt_public_transport_german_converter), # 'Mittelmäßig'
1315     ('Aufstieg möglich', opt_bool_german_converter), # 'Ja'
1316     ('Aufstieg getrennt', opt_tristate_german_comment_converter), # 'Ja'
1317     ('Gehzeit', opt_minutes_converter), # 90
1318     ('Aufstiegshilfe', lift_german_converter), # 'Gondel (unterer Teil)'
1319     ('Beleuchtungsanlage', opt_tristate_german_comment_converter),
1320     ('Beleuchtungstage', nightlightdays_converter), # '3 (Montag, Mittwoch, Freitag)'
1321     ('Rodelverleih', sledrental_converter), # 'Talstation Serlesbahnan'
1322     ('Gütesiegel', cachet_german_converter), # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
1323     ('Webauskunft', webauskunft_converter), # 'http://www.nösslachhütte.at/page9.php'
1324     ('Telefonauskunft', telefonauskunft_converter), # '+43-664-5487520 (Mitterer Alm)'
1325     ('Bild', opt_str_converter),
1326     ('In Übersichtskarte', opt_bool_german_converter),
1327     ('Forumid', opt_int_converter)
1328 ])
1329
1330
1331 def rodelbahnbox_from_template(template):
1332     return box_from_template(template, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
1333
1334
1335 def rodelbahnbox_to_template(value):
1336     return box_to_template(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
1337
1338
1339 def rodelbahnbox_from_str(value):
1340     return box_from_str(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
1341
1342
1343 def rodelbahnbox_to_str(value):
1344     template = rodelbahnbox_to_template(value)
1345     template_to_table(template, 20)
1346     return str(template)
1347
1348
1349 GASTHAUSBOX_TEMPLATE_NAME = 'Gasthausbox'
1350
1351
1352 GASTHAUSBOX_DICT = OrderedDict([
1353     ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
1354     ('Höhe', opt_meter_converter),
1355     ('Betreiber', opt_str_converter),
1356     ('Sitzplätze', opt_int_converter),
1357     ('Übernachtung', BoolUnicodeTupleValidator()),
1358     ('Rauchfrei', opt_tristate_german_validator),
1359     ('Rodelverleih', BoolUnicodeTupleValidator()),
1360     ('Handyempfang', ValueCommentListNeinLoopNone()),
1361     ('Homepage', webauskunft_converter),
1362     ('E-Mail', EmailCommentListNeinLoopNone(allow_masked_email=True)),
1363     ('Telefon', PhoneCommentListNeinLoopNone(comments_are_optional=True)),
1364     ('Bild', opt_str_converter),
1365     ('Rodelbahnen', WikiPageListLoopNone())])
1366
1367
1368
1369 def sledrun_page_title_to_pretty_url(page_title):
1370     """Converts a page_title from the page_title column of wrsledruncache to name_url.
1371     name_url is not used by MediaWiki but by new applications like wrweb."""
1372     return page_title.lower().replace(' ', '-').replace('_', '-').replace('(', '').replace(')', '')