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