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