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