2 # -*- coding: iso-8859-15 -*-
6 A converter is a Python variable (may be a class, class instance or anything else) that has the member
7 functions from_str and to_str. From string takes a string "from the outside", checks it and returns a Python variable
8 representing that value in Python. It reports error by raising ValueError. to_str does the opposite, however, it
9 can assume that the value it has to convert to a string is valid. If it gets an invalid value, the behavior is
13 import email.headerregistry
16 import xml.dom.minidom as minidom
17 from xml.parsers.expat import ExpatError
18 from collections import OrderedDict, namedtuple
20 import mwparserfromhell
22 import formencode.national
23 from wrpylib.mwmarkup import template_to_table
26 # Meta converter types and functions
27 # ----------------------------------
29 FromToConverter = namedtuple('FromToConverter', ['from_str', 'to_str'])
32 def opt_from_str(value, from_str, none=None):
33 return none if value == '' else from_str(value)
36 def opt_to_str(value, to_str, none=None):
37 return '' if value == none else to_str(value)
40 def choice_from_str(value, choices):
41 if value not in choices:
42 raise ValueError('{} is an invalid value')
46 def dictkey_from_str(value, key_str_dict):
48 return dict(list(zip(key_str_dict.values(), key_str_dict.keys())))[value]
50 raise ValueError("Invalid value '{}'".format(value))
53 def dictkey_to_str(value, key_str_dict):
55 return key_str_dict[value]
57 raise ValueError("Invalid value '{}'".format(value))
60 # Basic type converter functions
61 # ------------------------------
64 def str_from_str(value):
68 def str_to_str(value):
72 def opt_str_from_str(value):
73 return opt_from_str(value, str_from_str)
76 def opt_str_to_str(value):
77 return opt_to_str(value, str_to_str)
80 opt_str_converter = FromToConverter(opt_str_from_str, opt_str_to_str)
83 def req_str_from_str(value):
85 raise ValueError('missing required value')
86 return str_from_str(value)
89 def int_from_str(value, min=None, max=None):
91 if min is not None and value < min:
92 raise ValueError('{} must be >= than {}'.format(value, min))
93 if max is not None and value > max:
94 raise ValueError('{} must be <= than {}'.format(value, max))
98 def int_to_str(value):
102 def opt_int_from_str(value, min=None, max=None):
103 return opt_from_str(value, lambda val: int_from_str(val, min, max))
106 def opt_int_to_str(value):
107 return opt_to_str(value, int_to_str)
110 def opt_uint_from_str(value, min=0, max=None):
111 """Optional positive integer."""
112 return opt_int_from_str(value, min, max)
115 def opt_uint_to_str(value):
116 return opt_int_to_str(value)
119 opt_uint_converter = FromToConverter(opt_uint_from_str, opt_uint_to_str)
126 def enum_from_str(value, from_str=req_str_from_str, separator=';', min_len=0):
127 """Semicolon separated list of entries with the same "type"."""
128 values = value.split(separator)
129 if len(values) == 1 and values[0] == '':
131 if len(values) < min_len:
132 raise ValueError('at least {} entry/entries have to be in the enumeration'.format(min_len))
133 return list(map(from_str, map(str.strip, values)))
136 def enum_to_str(value, to_str=opt_str_to_str, separator='; '):
137 return separator.join(map(to_str, value))
140 # Specific converter functions
141 # ----------------------------
143 BOOL_GERMAN = OrderedDict([(False, 'Nein'), (True, 'Ja')])
146 def bool_german_from_str(value):
147 return dictkey_from_str(value, BOOL_GERMAN)
150 def bool_german_to_str(value):
151 return dictkey_to_str(value, BOOL_GERMAN)
154 def opt_bool_german_from_str(value):
155 return opt_from_str(value, bool_german_from_str)
158 def opt_bool_german_to_str(value):
159 return opt_to_str(value, bool_german_to_str)
162 opt_bool_german_converter = FromToConverter(opt_bool_german_from_str, opt_bool_german_to_str)
165 TRISTATE_GERMAN = OrderedDict([(0.0, 'Nein'), (0.5, 'Teilweise'), (1.0, 'Ja')])
168 def tristate_german_from_str(value):
169 return dictkey_from_str(value, TRISTATE_GERMAN)
172 def tristate_german_to_str(value):
173 return dictkey_to_str(value, TRISTATE_GERMAN)
176 def opt_tristate_german_from_str(value):
177 return opt_from_str(value, tristate_german_from_str)
180 def opt_tristate_german_to_str(value):
181 return opt_to_str(value, tristate_german_to_str)
184 opt_tristate_german_converter = FromToConverter(opt_tristate_german_from_str, opt_tristate_german_to_str)
187 LonLat = namedtuple('LonLat', ['lon', 'lat'])
190 lonlat_none = LonLat(None, None)
193 def lonlat_from_str(value):
194 """Converts a winterrodeln geo string like '47.076207 N 11.453553 E' (being '<latitude> N <longitude> E'
195 to the LonLat(lon, lat) named tupel."""
196 r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value)
197 if r is None: raise ValueError("Coordinates '{}' have not a format like '47.076207 N 11.453553 E'".format(value))
198 return LonLat(float(r.groups()[1]), float(r.groups()[0]))
201 def lonlat_to_str(value):
202 return '{:.6f} N {:.6f} E'.format(value.lat, value.lon)
205 def opt_lonlat_from_str(value):
206 return opt_from_str(value, lonlat_from_str, lonlat_none)
209 def opt_lonlat_to_str(value):
210 return opt_to_str(value, lonlat_to_str, lonlat_none)
213 opt_lonlat_converter = FromToConverter(opt_lonlat_from_str, opt_lonlat_to_str)
217 class MultiGeo(formencode.FancyValidator):
218 "Formats multiple coordinates, even in multiple lines to [(latitude, longitude, elevation), ...] or [(latitude, longitude, None), ...] tuplets."
220 # Valid for input_format
221 FORMAT_GUESS = 0 # guesses the input format; default for input_format
222 FORMAT_NONE = -1 # indicates missing formats
224 # Valid for input_format and output_format
225 FORMAT_GEOCACHING = 1 # e.g. "N 47° 13.692 E 011° 25.535"
226 FORMAT_WINTERRODELN = 2 # e.g. "47.222134 N 11.467211 E"
227 FORMAT_GMAPPLUGIN = 3 # e.g. "47.232922, 11.452239"
228 FORMAT_GPX = 4 # e.g. "<trkpt lat="47.181289" lon="11.408827"><ele>1090.57</ele></trkpt>"
230 input_format = FORMAT_GUESS
231 output_format = FORMAT_WINTERRODELN
232 last_input_format = FORMAT_NONE
234 def __init__(self, input_format = FORMAT_GUESS, output_format = FORMAT_WINTERRODELN, **keywords):
235 self.input_format = input_format
236 self.output_format = output_format
237 formencode.FancyValidator.__init__(self, if_empty = (None, None, None), **keywords)
239 def to_python(self, value, state=None):
240 self.assert_string(value, state)
241 input_format = self.input_format
242 if not input_format in [self.FORMAT_GUESS, self.FORMAT_GEOCACHING, self.FORMAT_WINTERRODELN, self.FORMAT_GMAPPLUGIN, self.FORMAT_GPX]:
243 raise formencode.Invalid("input_format %d is not recognized" % input_format, value, state) # Shouldn't it be an other type of runtime error?
244 lines = [line.strip() for line in value.split("\n") if len(line.strip()) > 0]
248 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GEOCACHING:
249 r = re.match('N ?(\d+)° ?(\d+\.\d+) +E ?(\d+)° ?(\d+\.\d+)', line)
252 result.append((float(g[0]) + float(g[1])/60, float(g[2]) + float(g[3])/60, None))
253 last_input_format = self.FORMAT_WINTERRODELN
256 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_WINTERRODELN:
257 r = re.match('(\d+\.\d+) N (\d+\.\d+) E', line)
259 result.append((float(r.groups()[0]), float(r.groups()[1]), None))
260 last_input_format = self.FORMAT_WINTERRODELN
263 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GMAPPLUGIN:
264 r = re.match('(\d+\.\d+), ?(\d+\.\d+)', line)
266 result.append((float(r.groups()[0]), float(r.groups()[1]), None))
267 last_input_format = self.FORMAT_GMAPPLUGIN
270 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GPX:
272 xml = minidom.parseString(line)
273 coord = xml.documentElement
274 lat = float(coord.getAttribute('lat'))
275 lon = float(coord.getAttribute('lon'))
276 try: ele = float(coord.childNodes[0].childNodes[0].nodeValue)
277 except (IndexError, ValueError): ele = None
278 result.append((lat, lon, ele))
279 last_input_format = self.FORMAT_GPX
281 except (ExpatError, IndexError, ValueError): pass
283 raise formencode.Invalid("Coordinates '%s' have no known format" % line, value, state)
287 def from_python(self, value, state=None):
288 output_format = self.output_format
290 for latitude, longitude, height in value:
291 if output_format == self.FORMAT_GEOCACHING:
293 result.append('N %02d° %02.3f E %03d° %02.3f' % (latitude, latitude % 1 * 60, longitude, longitude % 1 * 60))
295 elif output_format == self.FORMAT_WINTERRODELN:
296 result.append('%.6f N %.6f E' % (latitude, longitude))
298 elif output_format == self.FORMAT_GMAPPLUGIN:
299 result.append('%.6f, %.6f' % (latitude, longitude))
301 elif output_format == self.FORMAT_GPX:
302 if not height is None: result.append('<trkpt lat="%.6f" lon="%.6f"><ele>%.2f</ele></trkpt>' % (latitude, longitude, height))
303 else: result.append('<trkpt lat="%.6f" lon="%.6f"/>' % (latitude, longitude))
306 raise formencode.Invalid("output_format %d is not recognized" % output_format, value, state) # Shouldn't it be an other type of runtime error?
308 return "\n".join(result)
311 DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')])
314 def difficulty_german_from_str(value):
315 return dictkey_from_str(value, DIFFICULTY_GERMAN)
318 def difficulty_german_to_str(value):
319 return dictkey_to_str(value, DIFFICULTY_GERMAN)
322 def opt_difficulty_german_from_str(value):
323 return opt_from_str(value, difficulty_german_from_str)
326 def opt_difficulty_german_to_str(value):
327 return opt_to_str(value, difficulty_german_to_str)
330 opt_difficulty_german_converter = FromToConverter(opt_difficulty_german_from_str, opt_difficulty_german_to_str)
333 AVALANCHES_GERMAN = OrderedDict([(1, 'kaum'), (2, 'selten'), (3, 'gelegentlich'), (4, 'häufig')])
336 def avalanches_german_from_str(value):
337 return dictkey_from_str(value, AVALANCHES_GERMAN)
340 def avalanches_german_to_str(value):
341 return dictkey_to_str(value, AVALANCHES_GERMAN)
344 def opt_avalanches_german_from_str(value):
345 return opt_from_str(value, avalanches_german_from_str)
348 def opt_avalanches_german_to_str(value):
349 return opt_to_str(value, avalanches_german_to_str)
352 opt_avalanches_german_converter = FromToConverter(opt_avalanches_german_from_str, opt_avalanches_german_to_str)
355 PUBLIC_TRANSPORT_GERMAN = OrderedDict([(1, 'Sehr gut'), (2, 'Gut'), (3, 'Mittelmäßig'), (4, 'Schlecht'), (5, 'Nein'), (6, 'Ja')])
358 def public_transport_german_from_str(value):
359 return dictkey_from_str(value, PUBLIC_TRANSPORT_GERMAN)
362 def public_transport_german_to_str(value):
363 return dictkey_to_str(value, PUBLIC_TRANSPORT_GERMAN)
366 def opt_public_transport_german_from_str(value):
367 return opt_from_str(value, public_transport_german_from_str)
370 def opt_public_transport_german_to_str(value):
371 return opt_to_str(value, public_transport_german_to_str)
374 opt_public_transport_german_converter = FromToConverter(opt_public_transport_german_from_str, opt_public_transport_german_to_str)
377 def value_comment_from_str(value, value_from_str=str_from_str, comment_from_str=str_from_str, comment_optional=False):
378 """Makes it possible to have a mandatory comment in parenthesis at the end of the string."""
381 comment_end_pos = None
382 for i, char in enumerate(value[::-1]):
385 if open_brackets == 1:
387 if len(value[-1-comment_end_pos:].rstrip()) > 1:
388 raise ValueError('invalid characters after comment')
391 if open_brackets == 0:
392 comment = value[-i:-1-comment_end_pos]
393 value = value[:-i-1].rstrip()
396 if open_brackets > 0:
397 raise ValueError('bracket mismatch')
398 if not comment_optional:
399 raise ValueError('mandatory comment not found')
400 return value_from_str(value), comment_from_str(comment)
403 def value_comment_to_str(value, value_to_str=str_to_str, comment_to_str=str_to_str, comment_optional=False):
404 left = value_to_str(value[0])
405 comment = comment_to_str(value[1])
406 if len(comment) > 0 or not comment_optional:
407 comment = '({})'.format(comment)
410 if len(comment) == 0:
412 return '{} {}'.format(left, comment)
415 def opt_tristate_german_comment_from_str(value):
416 """Ja, Nein or Vielleicht, optionally with comment in parenthesis."""
417 return value_comment_from_str(value, opt_tristate_german_from_str, opt_str_from_str, True)
420 def opt_tristate_german_comment_to_str(value):
421 return value_comment_to_str(value, opt_tristate_german_to_str, opt_str_to_str, True)
424 opt_tristate_german_comment_converter = FromToConverter(opt_tristate_german_comment_from_str, opt_tristate_german_comment_to_str)
427 def no_german_from_str(value, from_str=req_str_from_str, use_tuple=True, no_value=None):
429 return (False, no_value) if use_tuple else no_value
430 return (True, from_str(value)) if use_tuple else from_str(value)
433 def no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None):
437 return to_str(value[1])
439 if value == no_value:
444 def opt_no_german_from_str(value, from_str=str_from_str, use_tuple=True, no_value=None, none=(None, None)):
445 return opt_from_str(value, lambda v: no_german_from_str(v, from_str, use_tuple, no_value), none)
448 def opt_no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None, none=(None, None)):
449 return opt_to_str(value, lambda v: no_german_to_str(v, to_str, use_tuple, no_value), none)
452 def night_light_from_str(value):
453 """'Beleuchtungsanlage' Tristate with optional comment:
456 'Teilweise' <=> (0.5, None)
457 'Nein' <=> (0.0, None)
458 'Ja (aber schmal)' <=> (1.0, 'aber schmal')
459 'Teilweise (oben)' <=> (0.5, 'oben')
460 'Nein (aber breit)' <=> (0.0, 'aber breit')
465 def nightlightdays_from_str(value):
466 return value_comment_from_str(value, lambda val: opt_from_str(val, lambda v: int_from_str(v, min=0, max=7)), opt_str_from_str, comment_optional=True)
469 def nightlightdays_to_str(value):
470 return value_comment_to_str(value, lambda val: opt_to_str(val, int_to_str), opt_str_to_str, comment_optional=True)
473 nightlightdays_converter = FromToConverter(nightlightdays_from_str, nightlightdays_to_str)
476 CACHET_REGEXP = [r'(Tiroler Naturrodelbahn-Gütesiegel) ([12]\d{3}) (leicht|mittel|schwer)$']
479 def single_cachet_german_from_str(value):
480 for pattern in CACHET_REGEXP:
481 match = re.match(pattern, value)
483 return match.groups()
484 raise ValueError("'{}' is no valid cachet".format(value))
487 def single_cachet_german_to_str(value):
488 return ' '.join(value)
491 def cachet_german_from_str(value):
492 """Converts a "Gütesiegel":
495 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' => [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]"""
496 return opt_no_german_from_str(value, lambda val: enum_from_str(val, single_cachet_german_from_str), False, [], None)
499 def cachet_german_to_str(value):
500 return opt_no_german_to_str(value, lambda val: enum_to_str(val, single_cachet_german_to_str), False, [], None)
503 cachet_german_converter = FromToConverter(cachet_german_from_str, cachet_german_to_str)
506 def url_from_str(value):
507 result = urllib.parse.urlparse(value)
508 if result.scheme not in ['http', 'https']:
509 raise ValueError('scheme has to be http or https')
510 if not result.netloc:
511 raise ValueError('url does not contain netloc')
515 def url_to_str(value):
519 def webauskunft_from_str(value):
520 return opt_no_german_from_str(value, url_from_str)
523 def webauskunft_to_str(value):
524 return opt_no_german_to_str(value, url_to_str)
527 webauskunft_converter = FromToConverter(webauskunft_from_str, webauskunft_to_str)
530 class Url(formencode.FancyValidator):
531 """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed."""
532 # formencode 1.2.5 to formencode 1.3.0a1 sometimes raise ValueError instead of Invalid exceptions
533 # https://github.com/formencode/formencode/pull/61
534 urlv = formencode.validators.URL()
536 def to_python(self, value, state=None):
537 self.assert_string(value, state)
539 v = v.replace('ä', 'a')
540 v = v.replace('ö', 'o')
541 v = v.replace('ü', 'u')
542 v = v.replace('ß', 'ss')
543 v = self.urlv.to_python(v, state)
546 def from_python(self, value, state=None):
550 def phone_number_from_str(value):
551 match = re.match(r'\+\d+(-\d+)*$', value)
553 raise ValueError('invalid format of phone number - use something like +43-699-1234567')
557 def phone_number_to_str(value):
561 def telefonauskunft_from_str(value):
562 return opt_no_german_from_str(value, lambda val: enum_from_str(val, lambda v: value_comment_from_str(v, phone_number_from_str, req_str_from_str, False)), False, [], None)
565 def telefonauskunft_to_str(value):
566 return opt_no_german_to_str(value, lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, phone_number_to_str, str_to_str)), False, [], None)
569 telefonauskunft_converter = FromToConverter(telefonauskunft_from_str, telefonauskunft_to_str)
572 def email_from_str(value):
573 """Takes an email address like 'office@example.com', checks it for correctness and returns it again as string."""
575 email.headerregistry.Address(addr_spec=value)
576 except email.errors.HeaderParseError as e:
577 raise ValueError('Invalid email address: {}'.format(value), e)
581 def email_to_str(value):
585 def masked_email_from_str(value, mask='(at)', masked_only=False):
586 """Converts an email address that is possibly masked. Returns a tuple. The first parameter is the un-masked
587 email address as string, the second is a boolean telling whether the address was masked."""
588 unmasked = value.replace(mask, '@')
589 was_masked = unmasked != value
590 if masked_only and not was_masked:
591 raise ValueError('E-Mail address not masked')
592 return email_from_str(unmasked), was_masked
595 def masked_email_to_str(value, mask='(at)'):
596 """Value is a tuple. The first entry is the email address, the second one is a boolean telling whether the
597 email address should be masked."""
598 email, do_masking = value
599 email = email_to_str(email)
601 email = email.replace('@', mask)
605 def emails_from_str(value):
606 return opt_no_german_from_str(value, lambda val: enum_from_str(val, lambda v: value_comment_from_str(v, masked_email_from_str, opt_str_from_str, True)), False, [], None)
609 def emails_to_str(value):
610 return opt_no_german_to_str(value, lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, masked_email_to_str, opt_str_to_str, True)), False, [], None)
613 emails_converter = FromToConverter(emails_from_str, email_to_str)
616 class MaskedEmail(formencode.FancyValidator):
617 """A masked email address as defined here is an email address that has the `@` character replacted by the text `(at)`.
618 So instead of `abd.def@example.com` it would be `abc.def(at)example.com`.
619 This validator takes either a normal or a masked email address in it's to_python method and returns the normal email address as well
620 as a bool indicating whether the email address was masked.
622 u'abc.def@example.com' <=> (u'abc.def@example.com', False)
623 u'abc.def(at)example.com' <=> (u'abc.def@example.com', True)
626 def __init__(self, *args, **kw):
627 if 'strip' not in kw: kw['strip'] = True
628 if 'not_empty' not in kw: kw['not_empty'] = False
629 if 'if_empty' not in kw: kw['if_empty'] = (None, None)
631 formencode.FancyValidator.__init__(self, *args, **kw)
633 def _to_python(self, value, state=None):
634 email = value.replace(self.at, '@')
635 masked = value != email
636 val_email = formencode.validators.Email()
637 return val_email.to_python(email, state), masked
639 def _from_python(self, value, state=None):
640 email, masked = value
641 if email is None: return ''
642 val_email = formencode.validators.Email()
643 email = val_email.from_python(email, state)
644 if masked: email = email.replace('@', self.at)
649 class EmailCommentListNeinLoopNone(NoneValidator):
650 """Converts a semicolon-separated list of email addresses with optional comments to itself.
651 The special value of u'Nein' indicates that there are no email addresses.
652 The empty string translates to None:
655 u'first@example.com' <=> u'first@example.com'
656 u'first@example.com (Nur Winter); second@example.com' <=> u'first@example.com (Nur Winter); second@example.com'
658 If the parameter allow_masked_email is true, the following gives no error:
659 u'abc.def(at)example.com (comment)' <=> u'abc.def(at)example.com (comment)'
661 def __init__(self, allow_masked_email=False):
662 NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(MaskedEmail() if allow_masked_email else formencode.validators.Email()))))
665 class WikiPage(formencode.FancyValidator):
666 """Validates wiki page name like u'[[Birgitzer Alm]]'.
667 The page is not checked for existance.
668 An empty string is an error.
669 u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
671 def to_python(self, value, state=None):
672 self.assert_string(value, state)
673 if not value.startswith('[[') or not value.endswith(']]'):
674 raise formencode.Invalid('No valid wiki page name', value, state)
677 def from_python(self, value, state=None):
682 class WikiPageList(SemicolonList):
683 """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]'.
684 u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> [u'[[Birgitzer Alm]]', u'[[Kemater Alm]]']
685 u'[[Birgitzer Alm]]' <=> [u'[[Birgitzer Alm]]']
689 SemicolonList.__init__(self, WikiPage())
694 class WikiPageListLoopNone(NoneValidator):
695 """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]' as string.
696 u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> u'[[Birgitzer Alm]]; [[Kemater Alm]]'
697 u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
701 NoneValidator.__init__(self, Loop(WikiPageList()))
705 LIFT_GERMAN = ['Sessellift', 'Gondel', 'Linienbus', 'Taxi', 'Sonstige']
708 def lift_german_from_str(value):
709 """Checks a lift_details property. It is a value comment property with the following
716 Alternatively, the value u'Nein' is allowed.
717 An empty string maps to (None, None).
722 'Sessellift <=> [('Sessellift', None)]
723 'Gondel (nur bis zur Hälfte)' <=> [('Gondel', 'nur bis zur Hälfte')]
724 'Sessellift; Taxi' <=> [('Sessellift', None), ('Taxi', None)]
725 'Sessellift (Wochenende); Taxi (6 Euro)' <=> [('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')]
727 return opt_no_german_from_str(value, lambda value_enum: enum_from_str(value_enum, lambda value_comment: value_comment_from_str(value_comment, lambda v: choice_from_str(v, LIFT_GERMAN), opt_str_from_str, comment_optional=True)), use_tuple=False, no_value=[], none=None)
730 def lift_german_to_str(value):
731 return opt_no_german_to_str(value, lambda value_enum: enum_to_str(value_enum, lambda value_comment: value_comment_to_str(value_comment, str_to_str, opt_str_to_str, comment_optional=True)), use_tuple=False, no_value=[], none=None)
734 lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str)
737 def sledrental_from_str(value):
738 """The value can be an empty string, 'Nein' or a semicolon-separated list of strings with optional comments.
741 'Talstation (nur mit Ticket); Schneealm' => [('Talstation', 'nur mit Ticket'), ('Schneealm', None)]"""
742 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)
745 def sledrental_to_str(value):
746 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)
749 sledrental_converter = FromToConverter(sledrental_from_str, sledrental_to_str)
752 def opt_no_or_str_from_str(value):
753 """Übernachtung. 'Nein' => (False, None); 'Nur Wochenende' => (True, 'Nur Wochenende'); 'Ja' => (True, 'Ja'); '' => (None, None)"""
754 return no_german_from_str(value, opt_str_from_str)
757 def opt_no_or_str_to_str(value):
758 return no_german_to_str(value, opt_str_to_str)
761 opt_no_or_str_converter = FromToConverter(opt_no_or_str_from_str, opt_no_or_str_to_str)
764 class ValueErrorList(ValueError):
768 def box_from_template(template, name, converter_dict):
769 if template.name.strip() != name:
770 raise ValueError('Box name has to be "{}"'.format(name))
771 result = OrderedDict()
772 exceptions_dict = OrderedDict()
774 for key, converter in converter_dict.items():
776 if not template.has(key):
777 raise ValueError('Missing parameter "{}"'.format(key))
778 result[key] = converter.from_str(str(template.get(key).value.strip()))
779 except ValueError as e:
780 exceptions_dict[key] = e
781 # check if keys are superfluous
782 superfluous_keys = {str(p.name.strip()) for p in template.params} - set(converter_dict.keys())
783 for key in superfluous_keys:
784 exceptions_dict[key] = ValueError('Superfluous parameter: "{}"'.format(key))
785 if len(exceptions_dict) > 0:
786 raise ValueErrorList('{} error(s) occurred when parsing template parameters.'.format(len(exceptions_dict)), exceptions_dict)
790 def box_to_template(value, name, converter_dict):
791 template = mwparserfromhell.nodes.template.Template(name)
792 for key, converter in converter_dict.items():
793 template.add(key, converter.to_str(value[key]))
797 def template_from_str(value, name):
798 wikicode = mwparserfromhell.parse(value)
799 template_list = wikicode.filter_templates(name)
801 raise ValueError('No "{}" template was found'.format(name))
802 if len(template_list) > 1:
803 raise ValueError('{} "{}" templates were found'.format(len(template_list), name))
804 return template_list[0]
807 def box_from_str(value, name, converter_dict):
808 template = template_from_str(value, name)
809 return box_from_template(template, name, converter_dict)
812 def box_to_str(value, name, converter_dict):
813 return str(box_to_template(value, name, converter_dict))
816 RODELBAHNBOX_TEMPLATE_NAME = 'Rodelbahnbox'
819 RODELBAHNBOX_DICT = OrderedDict([
820 ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
821 ('Position oben', opt_lonlat_converter), # '47.583333 N 15.75 E'
822 ('Höhe oben', opt_uint_converter), # '2000'
823 ('Position unten', opt_lonlat_converter), # '47.583333 N 15.75 E'
824 ('Höhe unten', opt_uint_converter), # '1200'
825 ('Länge', opt_uint_converter), # 3500
826 ('Schwierigkeit', opt_difficulty_german_converter), # 'mittel'
827 ('Lawinen', opt_avalanches_german_converter), # 'kaum'
828 ('Betreiber', opt_str_converter), # 'Max Mustermann'
829 ('Öffentliche Anreise', opt_public_transport_german_converter), # 'Mittelmäßig'
830 ('Aufstieg möglich', opt_bool_german_converter), # 'Ja'
831 ('Aufstieg getrennt', opt_tristate_german_comment_converter), # 'Ja'
832 ('Gehzeit', opt_uint_converter), # 90
833 ('Aufstiegshilfe', lift_german_converter), # 'Gondel (unterer Teil)'
834 ('Beleuchtungsanlage', opt_tristate_german_comment_converter),
835 ('Beleuchtungstage', nightlightdays_converter), # '3 (Montag, Mittwoch, Freitag)'
836 ('Rodelverleih', sledrental_converter), # 'Talstation Serlesbahnan'
837 ('Gütesiegel', cachet_german_converter), # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
838 ('Webauskunft', webauskunft_converter), # 'http://www.nösslachhütte.at/page9.php'
839 ('Telefonauskunft', telefonauskunft_converter), # '+43-664-5487520 (Mitterer Alm)'
840 ('Bild', opt_str_converter),
841 ('In Übersichtskarte', opt_bool_german_converter),
842 ('Forumid', opt_uint_converter)
846 def rodelbahnbox_from_template(template):
847 return box_from_template(template, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
850 def rodelbahnbox_to_template(value):
851 return box_to_template(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
854 def rodelbahnbox_from_str(value):
855 return box_from_str(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
858 def rodelbahnbox_to_str(value):
859 template = rodelbahnbox_to_template(value)
860 template_to_table(template, 20)
864 GASTHAUSBOX_TEMPLATE_NAME = 'Gasthausbox'
868 GASTHAUSBOX_DICT = OrderedDict([
869 ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
870 ('Höhe', opt_uint_converter),
871 ('Betreiber', opt_str_converter),
872 ('Sitzplätze', opt_uint_converter),
873 ('Übernachtung', opt_no_or_str_converter),
874 ('Rauchfrei', opt_tristate_german_converter),
875 ('Rodelverleih', opt_no_or_str_converter),
876 ('Handyempfang', ValueCommentListNeinLoopNone()),
877 ('Homepage', webauskunft_converter),
878 ('E-Mail', emails_converter),
879 ('Telefon', PhoneCommentListNeinLoopNone(comments_are_optional=True)),
880 ('Bild', opt_str_converter),
881 ('Rodelbahnen', WikiPageListLoopNone())])
885 def sledrun_page_title_to_pretty_url(page_title):
886 """Converts a page_title from the page_title column of wrsledruncache to name_url.
887 name_url is not used by MediaWiki but by new applications like wrweb."""
888 return page_title.lower().replace(' ', '-').replace('_', '-').replace('(', '').replace(')', '')