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