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 opt_int_converter = FromToConverter(opt_int_from_str, opt_int_to_str)
113 IntConverter = FromToConverter(int_from_str, int_to_str)
119 def enum_from_str(value, from_str=req_str_from_str, separator=';', min_len=0):
120 """Semicolon separated list of entries with the same "type"."""
121 values = value.split(separator)
122 if len(values) == 1 and values[0] == '':
124 if len(values) < min_len:
125 raise ValueError('at least {} entry/entries have to be in the enumeration'.format(min_len))
126 return list(map(from_str, map(str.strip, values)))
129 def enum_to_str(value, to_str=opt_str_to_str, separator='; '):
130 return separator.join(map(to_str, value))
133 # Specific converter functions
134 # ----------------------------
136 BOOL_GERMAN = OrderedDict([(False, 'Nein'), (True, 'Ja')])
139 def bool_german_from_str(value):
140 return dictkey_from_str(value, BOOL_GERMAN)
143 def bool_german_to_str(value):
144 return dictkey_to_str(value, BOOL_GERMAN)
147 def opt_bool_german_from_str(value):
148 return opt_from_str(value, bool_german_from_str)
151 def opt_bool_german_to_str(value):
152 return opt_to_str(value, bool_german_to_str)
155 opt_bool_german_converter = FromToConverter(opt_bool_german_from_str, opt_bool_german_to_str)
158 TRISTATE_GERMAN = OrderedDict([(0.0, 'Nein'), (0.5, 'Teilweise'), (1.0, 'Ja')])
161 def tristate_german_from_str(value):
162 return dictkey_from_str(value, TRISTATE_GERMAN)
165 def tristate_german_to_str(value):
166 return dictkey_to_str(value, TRISTATE_GERMAN)
169 def opt_tristate_german_from_str(value):
170 return opt_from_str(value, tristate_german_from_str)
173 def opt_tristate_german_to_str(value):
174 return opt_to_str(value, tristate_german_to_str)
177 def meter_from_str(value):
178 return int_from_str(value, min=0)
181 def meter_to_str(value):
182 return int_to_str(value)
185 def opt_meter_from_str(value):
186 return opt_from_str(value, meter_from_str)
189 def opt_meter_to_str(value):
190 return opt_to_str(value, meter_to_str)
193 opt_meter_converter = FromToConverter(opt_meter_from_str, opt_meter_to_str)
196 def minutes_from_str(value):
197 return int_from_str(value, min=0)
200 def minutes_to_str(value):
201 return int_to_str(value)
204 def opt_minutes_from_str(value):
205 return opt_from_str(value, minutes_from_str)
208 def opt_minutes_to_str(value):
209 return opt_to_str(value, minutes_to_str)
212 opt_minutes_converter = FromToConverter(opt_minutes_from_str, opt_minutes_to_str)
215 LonLat = namedtuple('LonLat', ['lon', 'lat'])
218 lonlat_none = LonLat(None, None)
221 def lonlat_from_str(value):
222 """Converts a winterrodeln geo string like '47.076207 N 11.453553 E' (being '<latitude> N <longitude> E'
223 to the LonLat(lon, lat) named tupel."""
224 r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value)
225 if r is None: raise ValueError("Coordinates '{}' have not a format like '47.076207 N 11.453553 E'".format(value))
226 return LonLat(float(r.groups()[1]), float(r.groups()[0]))
229 def lonlat_to_str(value):
230 return '{:.6f} N {:.6f} E'.format(value.lat, value.lon)
233 def opt_lonlat_from_str(value):
234 return opt_from_str(value, lonlat_from_str, lonlat_none)
237 def opt_lonlat_to_str(value):
238 return opt_to_str(value, lonlat_to_str, lonlat_none)
241 opt_lonlat_converter = FromToConverter(opt_lonlat_from_str, opt_lonlat_to_str)
245 class MultiGeo(formencode.FancyValidator):
246 "Formats multiple coordinates, even in multiple lines to [(latitude, longitude, elevation), ...] or [(latitude, longitude, None), ...] tuplets."
248 # Valid for input_format
249 FORMAT_GUESS = 0 # guesses the input format; default for input_format
250 FORMAT_NONE = -1 # indicates missing formats
252 # Valid for input_format and output_format
253 FORMAT_GEOCACHING = 1 # e.g. "N 47° 13.692 E 011° 25.535"
254 FORMAT_WINTERRODELN = 2 # e.g. "47.222134 N 11.467211 E"
255 FORMAT_GMAPPLUGIN = 3 # e.g. "47.232922, 11.452239"
256 FORMAT_GPX = 4 # e.g. "<trkpt lat="47.181289" lon="11.408827"><ele>1090.57</ele></trkpt>"
258 input_format = FORMAT_GUESS
259 output_format = FORMAT_WINTERRODELN
260 last_input_format = FORMAT_NONE
262 def __init__(self, input_format = FORMAT_GUESS, output_format = FORMAT_WINTERRODELN, **keywords):
263 self.input_format = input_format
264 self.output_format = output_format
265 formencode.FancyValidator.__init__(self, if_empty = (None, None, None), **keywords)
267 def to_python(self, value, state=None):
268 self.assert_string(value, state)
269 input_format = self.input_format
270 if not input_format in [self.FORMAT_GUESS, self.FORMAT_GEOCACHING, self.FORMAT_WINTERRODELN, self.FORMAT_GMAPPLUGIN, self.FORMAT_GPX]:
271 raise formencode.Invalid("input_format %d is not recognized" % input_format, value, state) # Shouldn't it be an other type of runtime error?
272 lines = [line.strip() for line in value.split("\n") if len(line.strip()) > 0]
276 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GEOCACHING:
277 r = re.match('N ?(\d+)° ?(\d+\.\d+) +E ?(\d+)° ?(\d+\.\d+)', line)
280 result.append((float(g[0]) + float(g[1])/60, float(g[2]) + float(g[3])/60, None))
281 last_input_format = self.FORMAT_WINTERRODELN
284 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_WINTERRODELN:
285 r = re.match('(\d+\.\d+) N (\d+\.\d+) E', line)
287 result.append((float(r.groups()[0]), float(r.groups()[1]), None))
288 last_input_format = self.FORMAT_WINTERRODELN
291 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GMAPPLUGIN:
292 r = re.match('(\d+\.\d+), ?(\d+\.\d+)', line)
294 result.append((float(r.groups()[0]), float(r.groups()[1]), None))
295 last_input_format = self.FORMAT_GMAPPLUGIN
298 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GPX:
300 xml = minidom.parseString(line)
301 coord = xml.documentElement
302 lat = float(coord.getAttribute('lat'))
303 lon = float(coord.getAttribute('lon'))
304 try: ele = float(coord.childNodes[0].childNodes[0].nodeValue)
305 except (IndexError, ValueError): ele = None
306 result.append((lat, lon, ele))
307 last_input_format = self.FORMAT_GPX
309 except (ExpatError, IndexError, ValueError): pass
311 raise formencode.Invalid("Coordinates '%s' have no known format" % line, value, state)
315 def from_python(self, value, state=None):
316 output_format = self.output_format
318 for latitude, longitude, height in value:
319 if output_format == self.FORMAT_GEOCACHING:
321 result.append('N %02d° %02.3f E %03d° %02.3f' % (latitude, latitude % 1 * 60, longitude, longitude % 1 * 60))
323 elif output_format == self.FORMAT_WINTERRODELN:
324 result.append('%.6f N %.6f E' % (latitude, longitude))
326 elif output_format == self.FORMAT_GMAPPLUGIN:
327 result.append('%.6f, %.6f' % (latitude, longitude))
329 elif output_format == self.FORMAT_GPX:
330 if not height is None: result.append('<trkpt lat="%.6f" lon="%.6f"><ele>%.2f</ele></trkpt>' % (latitude, longitude, height))
331 else: result.append('<trkpt lat="%.6f" lon="%.6f"/>' % (latitude, longitude))
334 raise formencode.Invalid("output_format %d is not recognized" % output_format, value, state) # Shouldn't it be an other type of runtime error?
336 return "\n".join(result)
339 DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')])
342 def difficulty_german_from_str(value):
343 return dictkey_from_str(value, DIFFICULTY_GERMAN)
346 def difficulty_german_to_str(value):
347 return dictkey_to_str(value, DIFFICULTY_GERMAN)
350 def opt_difficulty_german_from_str(value):
351 return opt_from_str(value, difficulty_german_from_str)
354 def opt_difficulty_german_to_str(value):
355 return opt_to_str(value, difficulty_german_to_str)
358 opt_difficulty_german_converter = FromToConverter(opt_difficulty_german_from_str, opt_difficulty_german_to_str)
361 AVALANCHES_GERMAN = OrderedDict([(1, 'kaum'), (2, 'selten'), (3, 'gelegentlich'), (4, 'häufig')])
364 def avalanches_german_from_str(value):
365 return dictkey_from_str(value, AVALANCHES_GERMAN)
368 def avalanches_german_to_str(value):
369 return dictkey_to_str(value, AVALANCHES_GERMAN)
372 def opt_avalanches_german_from_str(value):
373 return opt_from_str(value, avalanches_german_from_str)
376 def opt_avalanches_german_to_str(value):
377 return opt_to_str(value, avalanches_german_to_str)
380 opt_avalanches_german_converter = FromToConverter(opt_avalanches_german_from_str, opt_avalanches_german_to_str)
383 PUBLIC_TRANSPORT_GERMAN = OrderedDict([(1, 'Sehr gut'), (2, 'Gut'), (3, 'Mittelmäßig'), (4, 'Schlecht'), (5, 'Nein'), (6, 'Ja')])
386 def public_transport_german_from_str(value):
387 return dictkey_from_str(value, PUBLIC_TRANSPORT_GERMAN)
390 def public_transport_german_to_str(value):
391 return dictkey_to_str(value, PUBLIC_TRANSPORT_GERMAN)
394 def opt_public_transport_german_from_str(value):
395 return opt_from_str(value, public_transport_german_from_str)
398 def opt_public_transport_german_to_str(value):
399 return opt_to_str(value, public_transport_german_to_str)
402 opt_public_transport_german_converter = FromToConverter(opt_public_transport_german_from_str, opt_public_transport_german_to_str)
405 def value_comment_from_str(value, value_from_str=str_from_str, comment_from_str=str_from_str, comment_optional=False):
406 """Makes it possible to have a mandatory comment in parenthesis at the end of the string."""
409 comment_end_pos = None
410 for i, char in enumerate(value[::-1]):
413 if open_brackets == 1:
415 if len(value[-1-comment_end_pos:].rstrip()) > 1:
416 raise ValueError('invalid characters after comment')
419 if open_brackets == 0:
420 comment = value[-i:-1-comment_end_pos]
421 value = value[:-i-1].rstrip()
424 if open_brackets > 0:
425 raise ValueError('bracket mismatch')
426 if not comment_optional:
427 raise ValueError('mandatory comment not found')
428 return value_from_str(value), comment_from_str(comment)
431 def value_comment_to_str(value, value_to_str=str_to_str, comment_to_str=str_to_str, comment_optional=False):
432 left = value_to_str(value[0])
433 comment = comment_to_str(value[1])
434 if len(comment) > 0 or not comment_optional:
435 comment = '({})'.format(comment)
438 if len(comment) == 0:
440 return '{} {}'.format(left, comment)
443 def opt_tristate_german_comment_from_str(value):
444 """Ja, Nein or Vielleicht, optionally with comment in parenthesis."""
445 return value_comment_from_str(value, opt_tristate_german_from_str, opt_str_from_str, True)
448 def opt_tristate_german_comment_to_str(value):
449 return value_comment_to_str(value, opt_tristate_german_to_str, opt_str_to_str, True)
452 opt_tristate_german_comment_converter = FromToConverter(opt_tristate_german_comment_from_str, opt_tristate_german_comment_to_str)
455 def no_german_from_str(value, from_str=req_str_from_str, use_tuple=True, no_value=None):
457 return (False, no_value) if use_tuple else no_value
458 return (True, from_str(value)) if use_tuple else from_str(value)
461 def no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None):
465 return to_str(value[1])
467 if value == no_value:
472 def opt_no_german_from_str(value, from_str=str_from_str, use_tuple=True, no_value=None, none=(None, None)):
473 return opt_from_str(value, lambda v: no_german_from_str(v, from_str, use_tuple, no_value), none)
476 def opt_no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None, none=(None, None)):
477 return opt_to_str(value, lambda v: no_german_to_str(v, to_str, use_tuple, no_value), none)
480 def night_light_from_str(value):
481 """'Beleuchtungsanlage' Tristate with optional comment:
484 'Teilweise' <=> (0.5, None)
485 'Nein' <=> (0.0, None)
486 'Ja (aber schmal)' <=> (1.0, 'aber schmal')
487 'Teilweise (oben)' <=> (0.5, 'oben')
488 'Nein (aber breit)' <=> (0.0, 'aber breit')
493 def nightlightdays_from_str(value):
494 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)
497 def nightlightdays_to_str(value):
498 return value_comment_to_str(value, lambda val: opt_to_str(val, int_to_str), opt_str_to_str, comment_optional=True)
501 nightlightdays_converter = FromToConverter(nightlightdays_from_str, nightlightdays_to_str)
504 CACHET_REGEXP = [r'(Tiroler Naturrodelbahn-Gütesiegel) ([12]\d{3}) (leicht|mittel|schwer)$']
507 def single_cachet_german_from_str(value):
508 for pattern in CACHET_REGEXP:
509 match = re.match(pattern, value)
511 return match.groups()
512 raise ValueError("'{}' is no valid cachet".format(value))
515 def single_cachet_german_to_str(value):
516 return ' '.join(value)
519 def cachet_german_from_str(value):
520 """Converts a "Gütesiegel":
523 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' => [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]"""
524 return opt_no_german_from_str(value, lambda val: enum_from_str(val, single_cachet_german_from_str), False, [], None)
527 def cachet_german_to_str(value):
528 return opt_no_german_to_str(value, lambda val: enum_to_str(val, single_cachet_german_to_str), False, [], None)
531 cachet_german_converter = FromToConverter(cachet_german_from_str, cachet_german_to_str)
534 def url_from_str(value):
535 result = urllib.parse.urlparse(value)
536 if result.scheme not in ['http', 'https']:
537 raise ValueError('scheme has to be http or https')
538 if not result.netloc:
539 raise ValueError('url does not contain netloc')
543 def url_to_str(value):
547 def webauskunft_from_str(value):
548 return opt_no_german_from_str(value, url_from_str)
551 def webauskunft_to_str(value):
552 return opt_no_german_to_str(value, url_to_str)
555 webauskunft_converter = FromToConverter(webauskunft_from_str, webauskunft_to_str)
558 class Url(formencode.FancyValidator):
559 """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed."""
560 # formencode 1.2.5 to formencode 1.3.0a1 sometimes raise ValueError instead of Invalid exceptions
561 # https://github.com/formencode/formencode/pull/61
562 urlv = formencode.validators.URL()
564 def to_python(self, value, state=None):
565 self.assert_string(value, state)
567 v = v.replace('ä', 'a')
568 v = v.replace('ö', 'o')
569 v = v.replace('ü', 'u')
570 v = v.replace('ß', 'ss')
571 v = self.urlv.to_python(v, state)
574 def from_python(self, value, state=None):
578 def phone_number_from_str(value):
579 match = re.match(r'\+\d+(-\d+)*$', value)
581 raise ValueError('invalid format of phone number - use something like +43-699-1234567')
585 def phone_number_to_str(value):
589 def telefonauskunft_from_str(value):
590 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)
593 def telefonauskunft_to_str(value):
594 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)
597 telefonauskunft_converter = FromToConverter(telefonauskunft_from_str, telefonauskunft_to_str)
600 def email_from_str(value):
601 """Takes an email address like 'office@example.com', checks it for correctness and returns it again as string."""
603 email.headerregistry.Address(addr_spec=value)
604 except email.errors.HeaderParseError as e:
605 raise ValueError('Invalid email address: {}'.format(value), e)
609 def email_to_str(value):
613 def masked_email_from_str(value, mask='(at)', masked_only=False):
614 """Converts an email address that is possibly masked. Returns a tuple. The first parameter is the un-masked
615 email address as string, the second is a boolean telling whether the address was masked."""
616 unmasked = value.replace(mask, '@')
617 was_masked = unmasked != value
618 if masked_only and not was_masked:
619 raise ValueError('E-Mail address not masked')
620 return email_from_str(unmasked), was_masked
623 def masked_email_to_str(value, mask='(at)'):
624 """Value is a tuple. The first entry is the email address, the second one is a boolean telling whether the
625 email address should be masked."""
626 email, do_masking = value
627 email = email_to_str(email)
629 email = email.replace('@', mask)
633 def emails_from_str(value):
634 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)
637 def emails_to_str(value):
638 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)
641 class MaskedEmail(formencode.FancyValidator):
642 """A masked email address as defined here is an email address that has the `@` character replacted by the text `(at)`.
643 So instead of `abd.def@example.com` it would be `abc.def(at)example.com`.
644 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
645 as a bool indicating whether the email address was masked.
647 u'abc.def@example.com' <=> (u'abc.def@example.com', False)
648 u'abc.def(at)example.com' <=> (u'abc.def@example.com', True)
651 def __init__(self, *args, **kw):
652 if 'strip' not in kw: kw['strip'] = True
653 if 'not_empty' not in kw: kw['not_empty'] = False
654 if 'if_empty' not in kw: kw['if_empty'] = (None, None)
656 formencode.FancyValidator.__init__(self, *args, **kw)
658 def _to_python(self, value, state=None):
659 email = value.replace(self.at, '@')
660 masked = value != email
661 val_email = formencode.validators.Email()
662 return val_email.to_python(email, state), masked
664 def _from_python(self, value, state=None):
665 email, masked = value
666 if email is None: return ''
667 val_email = formencode.validators.Email()
668 email = val_email.from_python(email, state)
669 if masked: email = email.replace('@', self.at)
674 class EmailCommentListNeinLoopNone(NoneValidator):
675 """Converts a semicolon-separated list of email addresses with optional comments to itself.
676 The special value of u'Nein' indicates that there are no email addresses.
677 The empty string translates to None:
680 u'first@example.com' <=> u'first@example.com'
681 u'first@example.com (Nur Winter); second@example.com' <=> u'first@example.com (Nur Winter); second@example.com'
683 If the parameter allow_masked_email is true, the following gives no error:
684 u'abc.def(at)example.com (comment)' <=> u'abc.def(at)example.com (comment)'
686 def __init__(self, allow_masked_email=False):
687 NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(MaskedEmail() if allow_masked_email else formencode.validators.Email()))))
690 class WikiPage(formencode.FancyValidator):
691 """Validates wiki page name like u'[[Birgitzer Alm]]'.
692 The page is not checked for existance.
693 An empty string is an error.
694 u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
696 def to_python(self, value, state=None):
697 self.assert_string(value, state)
698 if not value.startswith('[[') or not value.endswith(']]'):
699 raise formencode.Invalid('No valid wiki page name', value, state)
702 def from_python(self, value, state=None):
707 class WikiPageList(SemicolonList):
708 """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]'.
709 u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> [u'[[Birgitzer Alm]]', u'[[Kemater Alm]]']
710 u'[[Birgitzer Alm]]' <=> [u'[[Birgitzer Alm]]']
714 SemicolonList.__init__(self, WikiPage())
719 class WikiPageListLoopNone(NoneValidator):
720 """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]' as string.
721 u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> u'[[Birgitzer Alm]]; [[Kemater Alm]]'
722 u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
726 NoneValidator.__init__(self, Loop(WikiPageList()))
730 LIFT_GERMAN = ['Sessellift', 'Gondel', 'Linienbus', 'Taxi', 'Sonstige']
733 def lift_german_from_str(value):
734 """Checks a lift_details property. It is a value comment property with the following
741 Alternatively, the value u'Nein' is allowed.
742 An empty string maps to (None, None).
747 'Sessellift <=> [('Sessellift', None)]
748 'Gondel (nur bis zur Hälfte)' <=> [('Gondel', 'nur bis zur Hälfte')]
749 'Sessellift; Taxi' <=> [('Sessellift', None), ('Taxi', None)]
750 'Sessellift (Wochenende); Taxi (6 Euro)' <=> [('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')]
752 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)
755 def lift_german_to_str(value):
756 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)
759 lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str)
762 def sledrental_from_str(value):
763 """The value can be an empty string, 'Nein' or a semicolon-separated list of strings with optional comments.
766 'Talstation (nur mit Ticket); Schneealm' => [('Talstation', 'nur mit Ticket'), ('Schneealm', None)]"""
767 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)
770 def sledrental_to_str(value):
771 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)
774 sledrental_converter = FromToConverter(sledrental_from_str, sledrental_to_str)
777 class ValueErrorList(ValueError):
781 def box_from_template(template, name, converter_dict):
782 if template.name.strip() != name:
783 raise ValueError('Box name has to be "{}"'.format(name))
784 result = OrderedDict()
785 exceptions_dict = OrderedDict()
787 for key, converter in converter_dict.items():
789 if not template.has(key):
790 raise ValueError('Missing parameter "{}"'.format(key))
791 result[key] = converter.from_str(str(template.get(key).value.strip()))
792 except ValueError as e:
793 exceptions_dict[key] = e
794 # check if keys are superfluous
795 superfluous_keys = {str(p.name.strip()) for p in template.params} - set(converter_dict.keys())
796 for key in superfluous_keys:
797 exceptions_dict[key] = ValueError('Superfluous parameter: "{}"'.format(key))
798 if len(exceptions_dict) > 0:
799 raise ValueErrorList('{} error(s) occurred when parsing template parameters.'.format(len(exceptions_dict)), exceptions_dict)
803 def box_to_template(value, name, converter_dict):
804 template = mwparserfromhell.nodes.template.Template(name)
805 for key, converter in converter_dict.items():
806 template.add(key, converter.to_str(value[key]))
810 def template_from_str(value, name):
811 wikicode = mwparserfromhell.parse(value)
812 template_list = wikicode.filter_templates(name)
814 raise ValueError('No "{}" template was found'.format(name))
815 if len(template_list) > 1:
816 raise ValueError('{} "{}" templates were found'.format(len(template_list), name))
817 return template_list[0]
820 def box_from_str(value, name, converter_dict):
821 template = template_from_str(value, name)
822 return box_from_template(template, name, converter_dict)
825 def box_to_str(value, name, converter_dict):
826 return str(box_to_template(value, name, converter_dict))
829 RODELBAHNBOX_TEMPLATE_NAME = 'Rodelbahnbox'
832 RODELBAHNBOX_DICT = OrderedDict([
833 ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
834 ('Position oben', opt_lonlat_converter), # '47.583333 N 15.75 E'
835 ('Höhe oben', opt_meter_converter), # '2000'
836 ('Position unten', opt_lonlat_converter), # '47.583333 N 15.75 E'
837 ('Höhe unten', opt_meter_converter), # '1200'
838 ('Länge', opt_meter_converter), # 3500
839 ('Schwierigkeit', opt_difficulty_german_converter), # 'mittel'
840 ('Lawinen', opt_avalanches_german_converter), # 'kaum'
841 ('Betreiber', opt_str_converter), # 'Max Mustermann'
842 ('Öffentliche Anreise', opt_public_transport_german_converter), # 'Mittelmäßig'
843 ('Aufstieg möglich', opt_bool_german_converter), # 'Ja'
844 ('Aufstieg getrennt', opt_tristate_german_comment_converter), # 'Ja'
845 ('Gehzeit', opt_minutes_converter), # 90
846 ('Aufstiegshilfe', lift_german_converter), # 'Gondel (unterer Teil)'
847 ('Beleuchtungsanlage', opt_tristate_german_comment_converter),
848 ('Beleuchtungstage', nightlightdays_converter), # '3 (Montag, Mittwoch, Freitag)'
849 ('Rodelverleih', sledrental_converter), # 'Talstation Serlesbahnan'
850 ('Gütesiegel', cachet_german_converter), # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
851 ('Webauskunft', webauskunft_converter), # 'http://www.nösslachhütte.at/page9.php'
852 ('Telefonauskunft', telefonauskunft_converter), # '+43-664-5487520 (Mitterer Alm)'
853 ('Bild', opt_str_converter),
854 ('In Übersichtskarte', opt_bool_german_converter),
855 ('Forumid', opt_int_converter)
859 def rodelbahnbox_from_template(template):
860 return box_from_template(template, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
863 def rodelbahnbox_to_template(value):
864 return box_to_template(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
867 def rodelbahnbox_from_str(value):
868 return box_from_str(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
871 def rodelbahnbox_to_str(value):
872 template = rodelbahnbox_to_template(value)
873 template_to_table(template, 20)
877 GASTHAUSBOX_TEMPLATE_NAME = 'Gasthausbox'
881 GASTHAUSBOX_DICT = OrderedDict([
882 ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
883 ('Höhe', opt_meter_converter),
884 ('Betreiber', opt_str_converter),
885 ('Sitzplätze', opt_int_converter),
886 ('Übernachtung', BoolUnicodeTupleValidator()),
887 ('Rauchfrei', opt_tristate_german_validator),
888 ('Rodelverleih', BoolUnicodeTupleValidator()),
889 ('Handyempfang', ValueCommentListNeinLoopNone()),
890 ('Homepage', webauskunft_converter),
891 ('E-Mail', EmailCommentListNeinLoopNone(allow_masked_email=True)),
892 ('Telefon', PhoneCommentListNeinLoopNone(comments_are_optional=True)),
893 ('Bild', opt_str_converter),
894 ('Rodelbahnen', WikiPageListLoopNone())])
898 def sledrun_page_title_to_pretty_url(page_title):
899 """Converts a page_title from the page_title column of wrsledruncache to name_url.
900 name_url is not used by MediaWiki but by new applications like wrweb."""
901 return page_title.lower().replace(' ', '-').replace('_', '-').replace('(', '').replace(')', '')