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