Implemented wikipage converter.
[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 # Meta converter types and functions
27 # ----------------------------------
28
29 FromToConverter = namedtuple('FromToConverter', ['from_str', 'to_str'])
30
31
32 def opt_from_str(value, from_str, none=None):
33     return none if value == '' else from_str(value)
34
35
36 def opt_to_str(value, to_str, none=None):
37     return '' if value == none else to_str(value)
38
39
40 def choice_from_str(value, choices):
41     if value not in choices:
42         raise ValueError('{} is an invalid value')
43     return value
44
45
46 def dictkey_from_str(value, key_str_dict):
47     try:
48         return dict(list(zip(key_str_dict.values(), key_str_dict.keys())))[value]
49     except KeyError:
50         raise ValueError("Invalid value '{}'".format(value))
51
52
53 def dictkey_to_str(value, key_str_dict):
54     try:
55         return key_str_dict[value]
56     except KeyError:
57         raise ValueError("Invalid value '{}'".format(value))
58
59
60 # Basic type converter functions
61 # ------------------------------
62
63
64 def str_from_str(value):
65     return value
66
67
68 def str_to_str(value):
69     return value
70
71
72 def opt_str_from_str(value):
73     return opt_from_str(value, str_from_str)
74
75
76 def opt_str_to_str(value):
77     return opt_to_str(value, str_to_str)
78
79
80 opt_str_converter = FromToConverter(opt_str_from_str, opt_str_to_str)
81
82
83 def req_str_from_str(value):
84     if value == '':
85         raise ValueError('missing required value')
86     return str_from_str(value)
87
88
89 def int_from_str(value, min=None, max=None):
90     value = int(value)
91     if min is not None and value < min:
92         raise ValueError('{} must be >= than {}'.format(value, min))
93     if max is not None and value > max:
94         raise ValueError('{} must be <= than {}'.format(value, max))
95     return value
96
97
98 def int_to_str(value):
99     return str(value)
100
101
102 def opt_int_from_str(value, min=None, max=None):
103     return opt_from_str(value, lambda val: int_from_str(val, min, max))
104
105
106 def opt_int_to_str(value):
107     return opt_to_str(value, int_to_str)
108
109
110 def opt_uint_from_str(value, min=0, max=None):
111     """Optional positive integer."""
112     return opt_int_from_str(value, min, max)
113
114
115 def opt_uint_to_str(value):
116     return opt_int_to_str(value)
117
118
119 opt_uint_converter = FromToConverter(opt_uint_from_str, opt_uint_to_str)
120
121
122
123 # Complex types
124 # -------------
125
126 def enum_from_str(value, from_str=req_str_from_str, separator=';', min_len=0):
127     """Semicolon separated list of entries with the same "type"."""
128     values = value.split(separator)
129     if len(values) == 1 and values[0] == '':
130         values = []
131     if len(values) < min_len:
132         raise ValueError('at least {} entry/entries have to be in the enumeration'.format(min_len))
133     return list(map(from_str, map(str.strip, values)))
134
135
136 def enum_to_str(value, to_str=opt_str_to_str, separator='; '):
137     return separator.join(map(to_str, value))
138
139
140 # Specific converter functions
141 # ----------------------------
142
143 BOOL_GERMAN = OrderedDict([(False, 'Nein'), (True, 'Ja')])
144
145
146 def bool_german_from_str(value):
147     return dictkey_from_str(value, BOOL_GERMAN)
148
149
150 def bool_german_to_str(value):
151     return dictkey_to_str(value, BOOL_GERMAN)
152
153
154 def opt_bool_german_from_str(value):
155     return opt_from_str(value, bool_german_from_str)
156
157
158 def opt_bool_german_to_str(value):
159     return opt_to_str(value, bool_german_to_str)
160
161
162 opt_bool_german_converter = FromToConverter(opt_bool_german_from_str, opt_bool_german_to_str)
163
164
165 TRISTATE_GERMAN = OrderedDict([(0.0, 'Nein'), (0.5, 'Teilweise'), (1.0, 'Ja')])
166
167
168 def tristate_german_from_str(value):
169     return dictkey_from_str(value, TRISTATE_GERMAN)
170
171
172 def tristate_german_to_str(value):
173     return dictkey_to_str(value, TRISTATE_GERMAN)
174
175
176 def opt_tristate_german_from_str(value):
177     return opt_from_str(value, tristate_german_from_str)
178
179
180 def opt_tristate_german_to_str(value):
181     return opt_to_str(value, tristate_german_to_str)
182
183
184 opt_tristate_german_converter = FromToConverter(opt_tristate_german_from_str, opt_tristate_german_to_str)
185
186
187 LonLat = namedtuple('LonLat', ['lon', 'lat'])
188
189
190 lonlat_none = LonLat(None, None)
191
192
193 def lonlat_from_str(value):
194     """Converts a winterrodeln geo string like '47.076207 N 11.453553 E' (being '<latitude> N <longitude> E'
195     to the LonLat(lon, lat) named  tupel."""
196     r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value)
197     if r is None: raise ValueError("Coordinates '{}' have not a format like '47.076207 N 11.453553 E'".format(value))
198     return LonLat(float(r.groups()[1]), float(r.groups()[0]))
199
200
201 def lonlat_to_str(value):
202     return '{:.6f} N {:.6f} E'.format(value.lat, value.lon)
203
204
205 def opt_lonlat_from_str(value):
206     return opt_from_str(value, lonlat_from_str, lonlat_none)
207
208
209 def opt_lonlat_to_str(value):
210     return opt_to_str(value, lonlat_to_str, lonlat_none)
211
212
213 opt_lonlat_converter = FromToConverter(opt_lonlat_from_str, opt_lonlat_to_str)
214
215
216
217 class MultiGeo(formencode.FancyValidator):
218     "Formats multiple coordinates, even in multiple lines to [(latitude, longitude, elevation), ...] or [(latitude, longitude, None), ...] tuplets."
219     
220     # Valid for input_format
221     FORMAT_GUESS = 0         # guesses the input format; default for input_format
222     FORMAT_NONE = -1          # indicates missing formats
223     
224     # Valid for input_format and output_format
225     FORMAT_GEOCACHING = 1    # e.g. "N 47° 13.692 E 011° 25.535"
226     FORMAT_WINTERRODELN = 2  # e.g. "47.222134 N 11.467211 E"
227     FORMAT_GMAPPLUGIN = 3    # e.g. "47.232922, 11.452239"
228     FORMAT_GPX = 4           # e.g. "<trkpt lat="47.181289" lon="11.408827"><ele>1090.57</ele></trkpt>"
229     
230     input_format = FORMAT_GUESS
231     output_format = FORMAT_WINTERRODELN
232     last_input_format = FORMAT_NONE
233
234     def __init__(self, input_format = FORMAT_GUESS, output_format = FORMAT_WINTERRODELN, **keywords):
235         self.input_format = input_format
236         self.output_format = output_format
237         formencode.FancyValidator.__init__(self, if_empty = (None, None, None), **keywords)
238     
239     def to_python(self, value, state=None):
240         self.assert_string(value, state)
241         input_format = self.input_format
242         if not input_format in [self.FORMAT_GUESS, self.FORMAT_GEOCACHING, self.FORMAT_WINTERRODELN, self.FORMAT_GMAPPLUGIN, self.FORMAT_GPX]:
243             raise formencode.Invalid("input_format %d is not recognized" % input_format, value, state) # Shouldn't it be an other type of runtime error?
244         lines = [line.strip() for line in value.split("\n") if len(line.strip()) > 0]
245         
246         result = []
247         for line in lines:
248             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GEOCACHING:
249                 r = re.match('N ?(\d+)° ?(\d+\.\d+) +E ?(\d+)° ?(\d+\.\d+)', line)
250                 if not r is None:
251                     g = r.groups()
252                     result.append((float(g[0]) + float(g[1])/60, float(g[2]) + float(g[3])/60, None))
253                     last_input_format = self.FORMAT_WINTERRODELN
254                     continue
255                     
256             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_WINTERRODELN:
257                 r = re.match('(\d+\.\d+) N (\d+\.\d+) E', line)
258                 if not r is None:
259                     result.append((float(r.groups()[0]), float(r.groups()[1]), None))
260                     last_input_format = self.FORMAT_WINTERRODELN
261                     continue
262                 
263             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GMAPPLUGIN:
264                 r = re.match('(\d+\.\d+), ?(\d+\.\d+)', line)
265                 if not r is None:
266                     result.append((float(r.groups()[0]), float(r.groups()[1]), None))
267                     last_input_format = self.FORMAT_GMAPPLUGIN
268                     continue
269                 
270             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GPX:
271                 try:
272                     xml = minidom.parseString(line)
273                     coord = xml.documentElement
274                     lat = float(coord.getAttribute('lat'))
275                     lon = float(coord.getAttribute('lon'))
276                     try: ele = float(coord.childNodes[0].childNodes[0].nodeValue)
277                     except (IndexError, ValueError): ele = None
278                     result.append((lat, lon, ele))
279                     last_input_format = self.FORMAT_GPX
280                     continue
281                 except (ExpatError, IndexError, ValueError): pass
282
283             raise formencode.Invalid("Coordinates '%s' have no known format" % line, value, state)
284             
285         return result
286     
287     def from_python(self, value, state=None):
288         output_format = self.output_format
289         result = []
290         for latitude, longitude, height in value:
291             if output_format == self.FORMAT_GEOCACHING:
292                 degree = latitude
293                 result.append('N %02d° %02.3f E %03d° %02.3f' % (latitude, latitude % 1 * 60, longitude, longitude % 1 * 60))
294                 
295             elif output_format == self.FORMAT_WINTERRODELN:
296                 result.append('%.6f N %.6f E' % (latitude, longitude))
297
298             elif output_format == self.FORMAT_GMAPPLUGIN:
299                 result.append('%.6f, %.6f' % (latitude, longitude))
300                 
301             elif output_format == self.FORMAT_GPX:
302                 if not height is None: result.append('<trkpt lat="%.6f" lon="%.6f"><ele>%.2f</ele></trkpt>' % (latitude, longitude, height))
303                 else: result.append('<trkpt lat="%.6f" lon="%.6f"/>' % (latitude, longitude))
304             
305             else:
306                 raise formencode.Invalid("output_format %d is not recognized" % output_format, value, state) # Shouldn't it be an other type of runtime error?
307             
308         return "\n".join(result)
309
310
311 DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')])
312
313
314 def difficulty_german_from_str(value):
315     return dictkey_from_str(value, DIFFICULTY_GERMAN)
316
317
318 def difficulty_german_to_str(value):
319     return dictkey_to_str(value, DIFFICULTY_GERMAN)
320
321
322 def opt_difficulty_german_from_str(value):
323     return opt_from_str(value, difficulty_german_from_str)
324
325
326 def opt_difficulty_german_to_str(value):
327     return opt_to_str(value, difficulty_german_to_str)
328
329
330 opt_difficulty_german_converter = FromToConverter(opt_difficulty_german_from_str, opt_difficulty_german_to_str)
331
332
333 AVALANCHES_GERMAN = OrderedDict([(1, 'kaum'), (2, 'selten'), (3, 'gelegentlich'), (4, 'häufig')])
334
335
336 def avalanches_german_from_str(value):
337     return dictkey_from_str(value, AVALANCHES_GERMAN)
338
339
340 def avalanches_german_to_str(value):
341     return dictkey_to_str(value, AVALANCHES_GERMAN)
342
343
344 def opt_avalanches_german_from_str(value):
345     return opt_from_str(value, avalanches_german_from_str)
346
347
348 def opt_avalanches_german_to_str(value):
349     return opt_to_str(value, avalanches_german_to_str)
350
351
352 opt_avalanches_german_converter = FromToConverter(opt_avalanches_german_from_str, opt_avalanches_german_to_str)
353
354
355 PUBLIC_TRANSPORT_GERMAN = OrderedDict([(1, 'Sehr gut'), (2, 'Gut'), (3, 'Mittelmäßig'), (4, 'Schlecht'), (5, 'Nein'), (6, 'Ja')])
356
357
358 def public_transport_german_from_str(value):
359     return dictkey_from_str(value, PUBLIC_TRANSPORT_GERMAN)
360
361
362 def public_transport_german_to_str(value):
363     return dictkey_to_str(value, PUBLIC_TRANSPORT_GERMAN)
364
365
366 def opt_public_transport_german_from_str(value):
367     return opt_from_str(value, public_transport_german_from_str)
368
369
370 def opt_public_transport_german_to_str(value):
371     return opt_to_str(value, public_transport_german_to_str)
372
373
374 opt_public_transport_german_converter = FromToConverter(opt_public_transport_german_from_str, opt_public_transport_german_to_str)
375
376
377 def value_comment_from_str(value, value_from_str=str_from_str, comment_from_str=str_from_str, comment_optional=False):
378     """Makes it possible to have a mandatory comment in parenthesis at the end of the string."""
379     open_brackets = 0
380     comment = ''
381     comment_end_pos = None
382     for i, char in enumerate(value[::-1]):
383         if char == ')':
384             open_brackets += 1
385             if open_brackets == 1:
386                 comment_end_pos = i
387                 if len(value[-1-comment_end_pos:].rstrip()) > 1:
388                     raise ValueError('invalid characters after comment')
389         elif char == '(':
390             open_brackets -= 1
391             if open_brackets == 0:
392                 comment = value[-i:-1-comment_end_pos]
393                 value = value[:-i-1].rstrip()
394                 break
395     else:
396         if open_brackets > 0:
397             raise ValueError('bracket mismatch')
398         if not comment_optional:
399             raise ValueError('mandatory comment not found')
400     return value_from_str(value), comment_from_str(comment)
401
402
403 def value_comment_to_str(value, value_to_str=str_to_str, comment_to_str=str_to_str, comment_optional=False):
404     left = value_to_str(value[0])
405     comment = comment_to_str(value[1])
406     if len(comment) > 0 or not comment_optional:
407         comment = '({})'.format(comment)
408     if len(left) == 0:
409         return comment
410     if len(comment) == 0:
411         return left
412     return '{} {}'.format(left, comment)
413
414
415 def opt_tristate_german_comment_from_str(value):
416     """Ja, Nein or Vielleicht, optionally with comment in parenthesis."""
417     return value_comment_from_str(value, opt_tristate_german_from_str, opt_str_from_str, True)
418
419
420 def opt_tristate_german_comment_to_str(value):
421     return value_comment_to_str(value, opt_tristate_german_to_str, opt_str_to_str, True)
422
423
424 opt_tristate_german_comment_converter = FromToConverter(opt_tristate_german_comment_from_str, opt_tristate_german_comment_to_str)
425
426
427 def no_german_from_str(value, from_str=req_str_from_str, use_tuple=True, no_value=None):
428     if value == 'Nein':
429         return (False, no_value) if use_tuple else no_value
430     return (True, from_str(value)) if use_tuple else from_str(value)
431
432
433 def no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None):
434     if use_tuple:
435         if not value[0]:
436             return 'Nein'
437         return to_str(value[1])
438     else:
439         if value == no_value:
440             return 'Nein'
441         return to_str(value)
442
443
444 def opt_no_german_from_str(value, from_str=str_from_str, use_tuple=True, no_value=None, none=(None, None)):
445     return opt_from_str(value, lambda v: no_german_from_str(v, from_str, use_tuple, no_value), none)
446
447
448 def opt_no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None, none=(None, None)):
449     return opt_to_str(value, lambda v: no_german_to_str(v, to_str, use_tuple, no_value), none)
450
451
452 def night_light_from_str(value):
453     """'Beleuchtungsanlage' Tristate with optional comment:
454     ''                  <=> (None, None)
455     'Ja'                <=> (1.0,  None)
456     'Teilweise'         <=> (0.5,  None)
457     'Nein'              <=> (0.0,  None)
458     'Ja (aber schmal)'  <=> (1.0,  'aber schmal')
459     'Teilweise (oben)'  <=> (0.5,  'oben')
460     'Nein (aber breit)' <=> (0.0,  'aber breit')
461     """
462     return
463
464
465 def nightlightdays_from_str(value):
466     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)
467
468
469 def nightlightdays_to_str(value):
470     return value_comment_to_str(value, lambda val: opt_to_str(val, int_to_str), opt_str_to_str, comment_optional=True)
471
472
473 nightlightdays_converter = FromToConverter(nightlightdays_from_str, nightlightdays_to_str)
474
475
476 CACHET_REGEXP = [r'(Tiroler Naturrodelbahn-Gütesiegel) ([12]\d{3}) (leicht|mittel|schwer)$']
477
478
479 def single_cachet_german_from_str(value):
480     for pattern in CACHET_REGEXP:
481         match = re.match(pattern, value)
482         if match:
483             return match.groups()
484     raise ValueError("'{}' is no valid cachet".format(value))
485
486
487 def single_cachet_german_to_str(value):
488     return ' '.join(value)
489
490
491 def cachet_german_from_str(value):
492     """Converts a "Gütesiegel":
493     '' => None
494     'Nein' => []
495     'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' => [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]"""
496     return opt_no_german_from_str(value, lambda val: enum_from_str(val, single_cachet_german_from_str), False, [], None)
497
498     
499 def cachet_german_to_str(value):
500     return opt_no_german_to_str(value, lambda val: enum_to_str(val, single_cachet_german_to_str), False, [], None)
501
502
503 cachet_german_converter = FromToConverter(cachet_german_from_str, cachet_german_to_str)
504
505
506 def url_from_str(value):
507     result = urllib.parse.urlparse(value)
508     if result.scheme not in ['http', 'https']:
509         raise ValueError('scheme has to be http or https')
510     if not result.netloc:
511         raise ValueError('url does not contain netloc')
512     return value
513
514
515 def url_to_str(value):
516     return value
517
518
519 def webauskunft_from_str(value):
520     return opt_no_german_from_str(value, url_from_str)
521
522
523 def webauskunft_to_str(value):
524     return opt_no_german_to_str(value, url_to_str)
525
526
527 webauskunft_converter = FromToConverter(webauskunft_from_str, webauskunft_to_str)
528
529
530 class Url(formencode.FancyValidator):
531     """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed."""
532     # formencode 1.2.5 to formencode 1.3.0a1 sometimes raise ValueError instead of Invalid exceptions
533     # https://github.com/formencode/formencode/pull/61
534     urlv = formencode.validators.URL()    
535
536     def to_python(self, value, state=None):
537         self.assert_string(value, state)
538         v = value
539         v = v.replace('ä', 'a')
540         v = v.replace('ö', 'o')
541         v = v.replace('ü', 'u')
542         v = v.replace('ß', 'ss')
543         v = self.urlv.to_python(v, state)
544         return value
545     
546     def from_python(self, value, state=None):
547         return value
548
549
550 def phone_number_from_str(value):
551     match = re.match(r'\+\d+(-\d+)*$', value)
552     if match is None:
553         raise ValueError('invalid format of phone number - use something like +43-699-1234567')
554     return value
555
556
557 def phone_number_to_str(value):
558     return value
559
560
561 def telefonauskunft_from_str(value):
562     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)
563
564
565 def telefonauskunft_to_str(value):
566     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)
567
568
569 telefonauskunft_converter = FromToConverter(telefonauskunft_from_str, telefonauskunft_to_str)
570
571
572 def email_from_str(value):
573     """Takes an email address like 'office@example.com', checks it for correctness and returns it again as string."""
574     try:
575         email.headerregistry.Address(addr_spec=value)
576     except email.errors.HeaderParseError as e:
577         raise ValueError('Invalid email address: {}'.format(value), e)
578     return value
579
580
581 def email_to_str(value):
582     return str(value)
583
584
585 def masked_email_from_str(value, mask='(at)', masked_only=False):
586     """Converts an email address that is possibly masked. Returns a tuple. The first parameter is the un-masked
587     email address as string, the second is a boolean telling whether the address was masked."""
588     unmasked = value.replace(mask, '@')
589     was_masked = unmasked != value
590     if masked_only and not was_masked:
591         raise ValueError('E-Mail address not masked')
592     return email_from_str(unmasked), was_masked
593
594
595 def masked_email_to_str(value, mask='(at)'):
596     """Value is a tuple. The first entry is the email address, the second one is a boolean telling whether the
597     email address should be masked."""
598     email, do_masking = value
599     email = email_to_str(email)
600     if do_masking:
601         email = email.replace('@', mask)
602     return email
603
604
605 def emails_from_str(value):
606     return opt_no_german_from_str(value, lambda val: enum_from_str(val, lambda v: value_comment_from_str(v, masked_email_from_str, opt_str_from_str, True)), False, [], None)
607
608
609 def emails_to_str(value):
610     return opt_no_german_to_str(value, lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, masked_email_to_str, opt_str_to_str, True)), False, [], None)
611
612
613 emails_converter = FromToConverter(emails_from_str, email_to_str)
614
615
616 def wikipage_from_str(value):
617     """Validates wiki page name like '[[Birgitzer Alm]]'.
618     The page is not checked for existance.
619     An empty string is an error.
620     '[[Birgitzer Alm]]' => '[[Birgitzer Alm]]'
621     """
622     if not value.startswith('[[') or not value.endswith(']]'):
623         raise ValueError('No valid wiki page name "{}"'.format(value))
624     return value
625
626
627 def wikipage_to_str(value):
628     return value
629
630
631 def opt_wikipage_enum_from_str(value):
632     """Validates a list of wiki pages like '[[Birgitzer Alm]]; [[Kemater Alm]]'.
633     '[[Birgitzer Alm]]; [[Kemater Alm]]' => ['[[Birgitzer Alm]]', '[[Kemater Alm]]']
634     '[[Birgitzer Alm]]'                  => ['[[Birgitzer Alm]]']
635     'Nein'                               => []
636     ''                                   => None
637     """
638     return opt_no_german_from_str(value, lambda val: enum_from_str(val, wikipage_from_str), False, [], None)
639
640
641 def opt_wikipage_enum_to_str(value):
642     return opt_no_german_to_str(value, lambda val: enum_to_str(val, wikipage_to_str), False, [], None)
643
644
645 opt_wikipage_enum_converter = FromToConverter(opt_wikipage_enum_from_str, opt_wikipage_enum_to_str)
646
647
648 LIFT_GERMAN = ['Sessellift', 'Gondel', 'Linienbus', 'Taxi', 'Sonstige']
649
650
651 def lift_german_from_str(value):
652     """Checks a lift_details property. It is a value comment property with the following
653     values allowed:
654     'Sessellift'
655     'Gondel'
656     'Linienbus'
657     'Taxi'
658     'Sonstige'
659     Alternatively, the value u'Nein' is allowed.
660     An empty string maps to (None, None).
661
662     Examples:
663     ''                                       <=> None
664     'Nein'                                   <=> []
665     'Sessellift                              <=> [('Sessellift', None)]
666     'Gondel (nur bis zur Hälfte)'            <=> [('Gondel', 'nur bis zur Hälfte')]
667     'Sessellift; Taxi'                       <=> [('Sessellift', None), ('Taxi', None)]
668     'Sessellift (Wochenende); Taxi (6 Euro)' <=> [('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')]
669     """
670     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)
671
672
673 def lift_german_to_str(value):
674     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)
675
676
677 lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str)
678
679
680 def opt_str_opt_comment_enum_from_str(value):
681     """The value can be an empty string, 'Nein' or a semicolon-separated list of strings with optional comments.
682     ''                                       => None
683     'Nein'                                   => []
684     'Talstation (nur mit Ticket); Schneealm' => [('Talstation', 'nur mit Ticket'), ('Schneealm', None)]"""
685     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)
686
687
688 def sledrental_to_str(value):
689     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)
690
691
692 opt_str_opt_comment_enum_converter = FromToConverter(opt_str_opt_comment_enum_from_str, sledrental_to_str)
693
694
695 def opt_no_or_str_from_str(value):
696     """Übernachtung. 'Nein' => (False, None); 'Nur Wochenende' => (True, 'Nur Wochenende'); 'Ja' => (True, 'Ja'); '' => (None, None)"""
697     return opt_no_german_from_str(value)
698
699
700 def opt_no_or_str_to_str(value):
701     return opt_no_german_to_str(value)
702
703
704 opt_no_or_str_converter = FromToConverter(opt_no_or_str_from_str, opt_no_or_str_to_str)
705
706
707 class ValueErrorList(ValueError):
708     pass
709
710
711 def box_from_template(template, name, converter_dict):
712     if template.name.strip() != name:
713         raise ValueError('Box name has to be "{}"'.format(name))
714     result = OrderedDict()
715     exceptions_dict = OrderedDict()
716     # check values
717     for key, converter in converter_dict.items():
718         try:
719             if not template.has(key):
720                 raise ValueError('Missing parameter "{}"'.format(key))
721             result[key] = converter.from_str(str(template.get(key).value.strip()))
722         except ValueError as e:
723             exceptions_dict[key] = e
724     # check if keys are superfluous
725     superfluous_keys = {str(p.name.strip()) for p in template.params} - set(converter_dict.keys())
726     for key in superfluous_keys:
727         exceptions_dict[key] = ValueError('Superfluous parameter: "{}"'.format(key))
728     if len(exceptions_dict) > 0:
729         raise ValueErrorList('{} error(s) occurred when parsing template parameters.'.format(len(exceptions_dict)), exceptions_dict)
730     return result
731
732
733 def box_to_template(value, name, converter_dict):
734     template = mwparserfromhell.nodes.template.Template(name)
735     for key, converter in converter_dict.items():
736         template.add(key, converter.to_str(value[key]))
737     return template
738
739
740 def template_from_str(value, name):
741     wikicode = mwparserfromhell.parse(value)
742     template_list = wikicode.filter_templates(name)
743     if len(name) == 0:
744         raise ValueError('No "{}" template was found'.format(name))
745     if len(template_list) > 1:
746         raise ValueError('{} "{}" templates were found'.format(len(template_list), name))
747     return template_list[0]
748
749
750 def box_from_str(value, name, converter_dict):
751     template = template_from_str(value, name)
752     return box_from_template(template, name, converter_dict)
753
754
755 def box_to_str(value, name, converter_dict):
756     return str(box_to_template(value, name, converter_dict))
757
758
759 RODELBAHNBOX_TEMPLATE_NAME = 'Rodelbahnbox'
760
761
762 RODELBAHNBOX_DICT = OrderedDict([
763     ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
764     ('Position oben', opt_lonlat_converter), # '47.583333 N 15.75 E'
765     ('Höhe oben', opt_uint_converter), # '2000'
766     ('Position unten', opt_lonlat_converter), # '47.583333 N 15.75 E'
767     ('Höhe unten', opt_uint_converter), # '1200'
768     ('Länge', opt_uint_converter), # 3500
769     ('Schwierigkeit', opt_difficulty_german_converter), # 'mittel'
770     ('Lawinen', opt_avalanches_german_converter), # 'kaum'
771     ('Betreiber', opt_str_converter), # 'Max Mustermann'
772     ('Öffentliche Anreise', opt_public_transport_german_converter), # 'Mittelmäßig'
773     ('Aufstieg möglich', opt_bool_german_converter), # 'Ja'
774     ('Aufstieg getrennt', opt_tristate_german_comment_converter), # 'Ja'
775     ('Gehzeit', opt_uint_converter), # 90
776     ('Aufstiegshilfe', lift_german_converter), # 'Gondel (unterer Teil)'
777     ('Beleuchtungsanlage', opt_tristate_german_comment_converter),
778     ('Beleuchtungstage', nightlightdays_converter), # '3 (Montag, Mittwoch, Freitag)'
779     ('Rodelverleih', opt_str_opt_comment_enum_converter), # 'Talstation Serlesbahnan'
780     ('Gütesiegel', cachet_german_converter), # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
781     ('Webauskunft', webauskunft_converter), # 'http://www.nösslachhütte.at/page9.php'
782     ('Telefonauskunft', telefonauskunft_converter), # '+43-664-5487520 (Mitterer Alm)'
783     ('Bild', opt_str_converter),
784     ('In Übersichtskarte', opt_bool_german_converter),
785     ('Forumid', opt_uint_converter)
786 ])
787
788
789 def rodelbahnbox_from_template(template):
790     return box_from_template(template, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
791
792
793 def rodelbahnbox_to_template(value):
794     return box_to_template(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
795
796
797 def rodelbahnbox_from_str(value):
798     return box_from_str(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
799
800
801 def rodelbahnbox_to_str(value):
802     template = rodelbahnbox_to_template(value)
803     template_to_table(template, 20)
804     return str(template)
805
806
807 GASTHAUSBOX_TEMPLATE_NAME = 'Gasthausbox'
808
809
810 GASTHAUSBOX_DICT = OrderedDict([
811     ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
812     ('Höhe', opt_uint_converter),
813     ('Betreiber', opt_str_converter),
814     ('Sitzplätze', opt_uint_converter),
815     ('Übernachtung', opt_no_or_str_converter),
816     ('Rauchfrei', opt_tristate_german_converter),
817     ('Rodelverleih', opt_no_or_str_converter),
818     ('Handyempfang', opt_str_opt_comment_enum_converter),
819     ('Homepage', webauskunft_converter),
820     ('E-Mail', emails_converter),
821     ('Telefon', None), # PhoneCommentListNeinLoopNone(comments_are_optional=True)),
822     ('Bild', opt_str_converter),
823     ('Rodelbahnen', opt_wikipage_enum_converter)])
824
825
826 def sledrun_page_title_to_pretty_url(page_title):
827     """Converts a page_title from the page_title column of wrsledruncache to name_url.
828     name_url is not used by MediaWiki but by new applications like wrweb."""
829     return page_title.lower().replace(' ', '-').replace('_', '-').replace('(', '').replace(')', '')