X-Git-Url: https://git.toastfreeware.priv.at/philipp/winterrodeln/wrpylib.git/blobdiff_plain/c3722db017f5f0022021fa47fedb3a0f3be49dce..c6d5b01aec149831e4900df8048df8d15ab7d566:/wrpylib/wrvalidators.py diff --git a/wrpylib/wrvalidators.py b/wrpylib/wrvalidators.py index 831f84c..0a18088 100644 --- a/wrpylib/wrvalidators.py +++ b/wrpylib/wrvalidators.py @@ -1,300 +1,245 @@ -#!/usr/bin/python2.6 +#!/usr/bin/python3.4 # -*- coding: iso-8859-15 -*- -"""This file contains "validators" that convert between string and python (database) representation -of properties used in the "Rodelbahnbox" and "Gasthausbox". -The "to_python" method has to get a unicode argument. +# $Id$ +# $HeadURL$ +""" +A converter is a Python variable (may be a class, class instance or anything else) that has the member +functions from_str and to_str. From string takes a string "from the outside", checks it and returns a Python variable +representing that value in Python. It reports error by raising ValueError. to_str does the opposite, however, it +can assume that the value it has to convert to a string is valid. If it gets an invalid value, the behavior is +undefined. """ -import formencode -import formencode.national import datetime +import email.headerregistry +import urllib.parse import re import xml.dom.minidom as minidom from xml.parsers.expat import ExpatError +from collections import OrderedDict, namedtuple +import mwparserfromhell +import formencode +import formencode.national +from wrpylib.mwmarkup import template_to_table -class NoneValidator(formencode.FancyValidator): - """Takes a validator and makes it possible that empty strings are mapped to None.""" - def __init__(self, validator, python_none=None): - self.validator = validator - self.python_none = python_none - - def to_python(self, value): - self.assert_string(value, None) - if value == u'': return self.python_none - return self.validator.to_python(value) - - def from_python(self, value): - if value == self.python_none: return u'' - return self.validator.from_python(value) - - -class NeinValidator(formencode.FancyValidator): - """Take an arbitrary validator and adds the possibility that the - string can be u'Nein'. - Example together with an UnsignedNone validator: - >>> v = NeinValidator(UnsignedNone()) - >>> v.to_python(u'') - None - >>> v.to_python(u'34') - 34 - >>> v.to_python(u'Nein') - u'Nein' - """ - def __init__(self, validator, python_no=u'Nein'): - self.validator = validator - self.python_no = python_no - - def to_python(self, value): - self.assert_string(value, None) - if value == u'Nein': return self.python_no - return self.validator.to_python(value) - - def from_python(self, value): - if value == self.python_no: return u'Nein' - return self.validator.from_python(value) +# Meta converter types and functions +# ---------------------------------- -class Unicode(formencode.FancyValidator): - """Converts an unicode string to an unicode string: - u'any string' <=> u'any string'""" - def to_python(self, value): - self.assert_string(value, None) - return unicode(value) +FromToConverter = namedtuple('FromToConverter', ['from_str', 'to_str']) - def from_python(self, value): - return unicode(value) +def opt_from_str(value, from_str, none=None): + return none if value == '' else from_str(value) -class UnicodeNone(NoneValidator): - """Converts an unicode string to an unicode string: - u'' <=> None - u'any string' <=> u'any string'""" - def __init__(self): - NoneValidator.__init__(self, Unicode()) +def opt_to_str(value, to_str, none=None): + return '' if value == none else to_str(value) -class Unsigned(formencode.FancyValidator): - """Converts an unsigned number to a string and vice versa: - u'0' <=> 0 - u'1' <=> 1 - u'45' <=> 45 - """ - def __init__(self, max=None): - self.iv = formencode.validators.Int(min=0, max=max) - def to_python(self, value): - self.assert_string(value, None) - return self.iv.to_python(value) - - def from_python(self, value): - return unicode(value) +def choice_from_str(value, choices): + if value not in choices: + raise ValueError('{} is an invalid value') + return value -class UnsignedNone(NoneValidator): - """Converts an unsigned number to a string and vice versa: - u'' <=> None - u'0' <=> 0 - u'1' <=> 1 - u'45' <=> 45 - """ - def __init__(self, max=None): - NoneValidator.__init__(self, Unsigned(max)) +def dictkey_from_str(value, key_str_dict): + try: + return dict(list(zip(key_str_dict.values(), key_str_dict.keys())))[value] + except KeyError: + raise ValueError("Invalid value '{}'".format(value)) -class UnsignedNeinNone(NoneValidator): - """ Translates a number of Nein to a number. - u'' <=> None - u'Nein' <=> 0 - u'1' <=> 1 - u'2' <=> 2 - ... - """ - def __init__(self): - NoneValidator.__init__(self, UnsignedNone()) +def dictkey_to_str(value, key_str_dict): + try: + return key_str_dict[value] + except KeyError: + raise ValueError("Invalid value '{}'".format(value)) -class Loop(formencode.FancyValidator): - """Takes a validator and calls from_python(to_python(value)).""" - def __init__(self, validator): - self.validator = validator +# Basic type converter functions +# ------------------------------ - def to_python(self, value): - self.assert_string(value, None) - return self.validator.from_python(self.validator.to_python(value)) - - def from_python(self, value): - # we don't call self.validator.to_python(self.validator.from_python(value)) - # here because our to_python implementation basically leaves the input untouches - # and so should from_python do. - return self.validator.from_python(self.validator.to_python(value)) - - -class DictValidator(formencode.FancyValidator): - """Translates strings to other values via a python directory. - >>> boolValidator = DictValidator({u'': None, u'Ja': True, u'Nein': False}) - >>> boolValidator.to_python(u'') - None - >>> boolValidator.to_python(u'Ja') - True - """ - def __init__(self, dict): - self.dict = dict - - def to_python(self, value): - self.assert_string(value, None) - if not self.dict.has_key(value): raise formencode.Invalid("Key not found in dict.", value, None) - return self.dict[value] - - def from_python(self, value): - for k, v in self.dict.iteritems(): - if type(v) == type(value) and v == value: return k - raise formencode.Invalid('Invalid value', value, None) +def str_from_str(value): + return value -class GermanBoolNone(DictValidator): - """Converts German bool values to the python bool type: - u'' <=> None - u'Ja' <=> True - u'Nein' <=> False - """ - def __init__(self): - DictValidator.__init__(self, {u'': None, u'Ja': True, u'Nein': False}) - - -class GermanTristateTuple(DictValidator): - """Does the following conversion: - u'' <=> (None, None) - u'Ja' <=> (True, False) - u'Teilweise' <=> (True, True) - u'Nein' <=> (False, True)""" - def __init__(self, yes_python = (True, False), no_python = (False, True), partly_python = (True, True), none_python = (None, None)): - DictValidator.__init__(self, {u'': none_python, u'Ja': yes_python, u'Nein': no_python, u'Teilweise': partly_python}) - - -class GermanTristateFloat(GermanTristateTuple): - """Converts the a property with the possible values 0.0, 0.5, 1.0 or None - to a German text: - u'' <=> None - u'Ja' <=> 1.0 - u'Teilweise' <=> 0.5 - u'Nein' <=> 0.0""" - def __init__(self): - GermanTristateTuple.__init__(self, yes_python=1.0, no_python=0.0, partly_python=0.5, none_python=None) - - -class ValueComment(formencode.FancyValidator): - """Converts value with a potentially optional comment to a python tuple: - u'' <=> (None, None) - u'value' <=> (u'value', None) - u'value (comment)' <=> (u'value', u'comment')""" - def __init__(self, value_validator=UnicodeNone(), comment_validator=UnicodeNone(), comment_is_optional=True): - self.value_validator = value_validator - self.comment_validator = comment_validator - self.comment_is_optional = comment_is_optional - - def to_python(self, value): - self.assert_string(value, None) - if value == u'': - v = value - c = value - else: - left = value.find('(') - right = value.rfind(')') - if left < 0 and right < 0: - if not self.comment_is_optional: raise formencode.Invalid(u'Mandatory comment not present', value, None) - v = value - c = u'' - elif left >= 0 and right >= 0 and left < right: - v = value[:left].strip() - c = value[left+1:right].strip() - else: raise formencode.Invalid(u'Invalid format', value, None) - return self.value_validator.to_python(v), self.comment_validator.to_python(c) - - def from_python(self, value): - assert len(value) == 2 - v = self.value_validator.from_python(value[0]) - c = self.comment_validator.from_python(value[1]) - if len(c) > 0: - if len(v) > 0: return u'%s (%s)' % (v, c) - else: return u'(%s)' % c - return v - - -class SemicolonList(formencode.FancyValidator): - """Applies a given validator to a semicolon separated list of values and returns a python list. - For an empty string an empty list is returned.""" - def __init__(self, validator=Unicode()): - self.validator = validator - - def to_python(self, value): - self.assert_string(value, None) - return [self.validator.to_python(s.strip()) for s in value.split(';')] - - def from_python(self, value): - return "; ".join([self.validator.from_python(s) for s in value]) - - -class ValueCommentList(SemicolonList): - """A value-comment list looks like one of the following lines: - value - value (optional comment) - value1; value2 - value1; value2 (optional comment) - value1 (optional comment1); value2 (optional comment2); value3 (otional comment3) - value1 (optional comment1); value2 (optional comment2); value3 (otional comment3) - This function returns the value-comment list as list of tuples: - [(u'value1', u'comment1'), (u'value2', None)] - If no comment is present, None is specified. - For an empty string, [] is returned.""" - def __init__(self, value_validator=Unicode(), comments_are_optional=True): - SemicolonList.__init__(self, ValueComment(value_validator, comment_is_optional=comments_are_optional)) - - -class GenericDateTime(formencode.FancyValidator): - """Converts a generic date/time information to a datetime class with a user defined format. - '2009-03-22 20:36:15' would be specified as '%Y-%m-%d %H:%M:%S'.""" - - def __init__(self, date_time_format = '%Y-%m-%d %H:%M:%S', **keywords): - formencode.FancyValidator.__init__(self, **keywords) - self.date_time_format = date_time_format - - def to_python(self, value, state=None): - self.assert_string(value, None) - try: return datetime.datetime.strptime(value, self.date_time_format) - except ValueError, e: raise formencode.Invalid(str(e), value, None) - - def from_python(self, value, state=None): - return value.strftime(self.date_time_format) +def str_to_str(value): + return value -class DateTimeNoSec(GenericDateTime): - def __init__(self, **keywords): - GenericDateTime.__init__(self, '%Y-%m-%d %H:%M', **keywords) +def opt_str_from_str(value): + return opt_from_str(value, str_from_str) -class DateNone(NoneValidator): - """Converts date information to date classes with the format '%Y-%m-%d' or None.""" - def __init__(self): - NoneValidator.__init__(self, GenericDateTime('%Y-%m-%d')) +def opt_str_to_str(value): + return opt_to_str(value, str_to_str) -class Geo(formencode.FancyValidator): - """Formats to coordinates '47.076207 N 11.453553 E' to the (latitude, longitude) tuplet.""" - def to_python(self, value): - self.assert_string(value, None) - r = re.match(u'(\d+\.\d+) N (\d+\.\d+) E', value) - if r is None: raise formencode.Invalid(u"Coordinates '%s' have not a format like '47.076207 N 11.453553 E'" % value, value, None) - return (float(r.groups()[0]), float(r.groups()[1])) - - def from_python(self, value): - latitude, longitude = value - return u'%.6f N %.6f E' % (latitude, longitude) +opt_str_converter = FromToConverter(opt_str_from_str, opt_str_to_str) + + +def req_str_from_str(value): + if value == '': + raise ValueError('missing required value') + return str_from_str(value) + + +def int_from_str(value, min=None, max=None): + value = int(value) + if min is not None and value < min: + raise ValueError('{} must be >= than {}'.format(value, min)) + if max is not None and value > max: + raise ValueError('{} must be <= than {}'.format(value, max)) + return value + + +def int_to_str(value): + return str(value) + + +def opt_int_from_str(value, min=None, max=None): + return opt_from_str(value, lambda val: int_from_str(val, min, max)) + + +def opt_int_to_str(value): + return opt_to_str(value, int_to_str) + + +opt_int_converter = FromToConverter(opt_int_from_str, opt_int_to_str) + + +IntConverter = FromToConverter(int_from_str, int_to_str) + + +# Complex types +# ------------- + +def enum_from_str(value, from_str=req_str_from_str, separator=';', min_len=0): + """Semicolon separated list of entries with the same "type".""" + values = value.split(separator) + if len(values) == 1 and values[0] == '': + values = [] + if len(values) < min_len: + raise ValueError('at least {} entry/entries have to be in the enumeration'.format(min_len)) + return list(map(from_str, map(str.strip, values))) + + +def enum_to_str(value, to_str=opt_str_to_str, separator='; '): + return separator.join(map(to_str, value)) + + +# Specific converter functions +# ---------------------------- + +BOOL_GERMAN = OrderedDict([(False, 'Nein'), (True, 'Ja')]) + + +def bool_german_from_str(value): + return dictkey_from_str(value, BOOL_GERMAN) + + +def bool_german_to_str(value): + return dictkey_to_str(value, BOOL_GERMAN) + + +def opt_bool_german_from_str(value): + return opt_from_str(value, bool_german_from_str) + + +def opt_bool_german_to_str(value): + return opt_to_str(value, bool_german_to_str) + + +opt_bool_german_converter = FromToConverter(opt_bool_german_from_str, opt_bool_german_to_str) + + +TRISTATE_GERMAN = OrderedDict([(0.0, 'Nein'), (0.5, 'Teilweise'), (1.0, 'Ja')]) + + +def tristate_german_from_str(value): + return dictkey_from_str(value, TRISTATE_GERMAN) + + +def tristate_german_to_str(value): + return dictkey_to_str(value, TRISTATE_GERMAN) + + +def opt_tristate_german_from_str(value): + return opt_from_str(value, tristate_german_from_str) + + +def opt_tristate_german_to_str(value): + return opt_to_str(value, tristate_german_to_str) + + +def meter_from_str(value): + return int_from_str(value, min=0) + + +def meter_to_str(value): + return int_to_str(value) + + +def opt_meter_from_str(value): + return opt_from_str(value, meter_from_str) + + +def opt_meter_to_str(value): + return opt_to_str(value, meter_to_str) + + +opt_meter_converter = FromToConverter(opt_meter_from_str, opt_meter_to_str) + + +def minutes_from_str(value): + return int_from_str(value, min=0) + + +def minutes_to_str(value): + return int_to_str(value) + + +def opt_minutes_from_str(value): + return opt_from_str(value, minutes_from_str) + + +def opt_minutes_to_str(value): + return opt_to_str(value, minutes_to_str) + + +opt_minutes_converter = FromToConverter(opt_minutes_from_str, opt_minutes_to_str) + + +LonLat = namedtuple('LonLat', ['lon', 'lat']) + + +lonlat_none = LonLat(None, None) + + +def lonlat_from_str(value): + """Converts a winterrodeln geo string like '47.076207 N 11.453553 E' (being ' N E' + to the LonLat(lon, lat) named tupel.""" + r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value) + if r is None: raise ValueError("Coordinates '{}' have not a format like '47.076207 N 11.453553 E'".format(value)) + return LonLat(float(r.groups()[1]), float(r.groups()[0])) + + +def lonlat_to_str(value): + return '{:.6f} N {:.6f} E'.format(value.lat, value.lon) + + +def opt_lonlat_from_str(value): + return opt_from_str(value, lonlat_from_str, lonlat_none) + + +def opt_lonlat_to_str(value): + return opt_to_str(value, lonlat_to_str, lonlat_none) + + +opt_lonlat_converter = FromToConverter(opt_lonlat_from_str, opt_lonlat_to_str) -class GeoNone(NoneValidator): - """Formats to coordinates '47.076207 N 11.453553 E' to the (latitude, longitude) tuplet.""" - def __init__(self): - NoneValidator.__init__(self, Geo(), (None, None)) class MultiGeo(formencode.FancyValidator): @@ -319,17 +264,17 @@ class MultiGeo(formencode.FancyValidator): self.output_format = output_format formencode.FancyValidator.__init__(self, if_empty = (None, None, None), **keywords) - def to_python(self, value): - self.assert_string(value, None) + def to_python(self, value, state=None): + self.assert_string(value, state) input_format = self.input_format if not input_format in [self.FORMAT_GUESS, self.FORMAT_GEOCACHING, self.FORMAT_WINTERRODELN, self.FORMAT_GMAPPLUGIN, self.FORMAT_GPX]: - raise formencode.Invalid(u"input_format %d is not recognized" % input_format, value, None) # Shouldn't it be an other type of runtime error? + raise formencode.Invalid("input_format %d is not recognized" % input_format, value, state) # Shouldn't it be an other type of runtime error? lines = [line.strip() for line in value.split("\n") if len(line.strip()) > 0] result = [] for line in lines: if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GEOCACHING: - r = re.match(u'N ?(\d+)° ?(\d+\.\d+) +E ?(\d+)° ?(\d+\.\d+)', line) + r = re.match('N ?(\d+)° ?(\d+\.\d+) +E ?(\d+)° ?(\d+\.\d+)', line) if not r is None: g = r.groups() result.append((float(g[0]) + float(g[1])/60, float(g[2]) + float(g[3])/60, None)) @@ -337,14 +282,14 @@ class MultiGeo(formencode.FancyValidator): continue if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_WINTERRODELN: - r = re.match(u'(\d+\.\d+) N (\d+\.\d+) E', line) + r = re.match('(\d+\.\d+) N (\d+\.\d+) E', line) if not r is None: result.append((float(r.groups()[0]), float(r.groups()[1]), None)) last_input_format = self.FORMAT_WINTERRODELN continue if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GMAPPLUGIN: - r = re.match(u'(\d+\.\d+), ?(\d+\.\d+)', line) + r = re.match('(\d+\.\d+), ?(\d+\.\d+)', line) if not r is None: result.append((float(r.groups()[0]), float(r.groups()[1]), None)) last_input_format = self.FORMAT_GMAPPLUGIN @@ -363,235 +308,343 @@ class MultiGeo(formencode.FancyValidator): continue except (ExpatError, IndexError, ValueError): pass - raise formencode.Invalid(u"Coordinates '%s' have no known format" % line, value, None) + raise formencode.Invalid("Coordinates '%s' have no known format" % line, value, state) return result - def from_python(self, value): + def from_python(self, value, state=None): output_format = self.output_format result = [] for latitude, longitude, height in value: if output_format == self.FORMAT_GEOCACHING: degree = latitude - result.append(u'N %02d° %02.3f E %03d° %02.3f' % (latitude, latitude % 1 * 60, longitude, longitude % 1 * 60)) + result.append('N %02d° %02.3f E %03d° %02.3f' % (latitude, latitude % 1 * 60, longitude, longitude % 1 * 60)) elif output_format == self.FORMAT_WINTERRODELN: - result.append(u'%.6f N %.6f E' % (latitude, longitude)) + result.append('%.6f N %.6f E' % (latitude, longitude)) elif output_format == self.FORMAT_GMAPPLUGIN: - result.append(u'%.6f, %.6f' % (latitude, longitude)) + result.append('%.6f, %.6f' % (latitude, longitude)) elif output_format == self.FORMAT_GPX: - if not height is None: result.append(u'%.2f' % (latitude, longitude, height)) - else: result.append(u'' % (latitude, longitude)) + if not height is None: result.append('%.2f' % (latitude, longitude, height)) + else: result.append('' % (latitude, longitude)) else: - raise formencode.Invalid(u"output_format %d is not recognized" % output_format, value, None) # Shouldn't it be an other type of runtime error? + raise formencode.Invalid("output_format %d is not recognized" % output_format, value, state) # Shouldn't it be an other type of runtime error? return "\n".join(result) -# deprecated -class AustrianPhoneNumber(formencode.FancyValidator): - """ - Validates and converts phone numbers to +##/###/####### or +##/###/#######-### (having an extension) - @param default_cc country code for prepending if none is provided, defaults to 43 (Austria) - :: - >>> v = AustrianPhoneNumber() - >>> v.to_python(u'0512/12345678') - u'+43/512/12345678' - >>> v.to_python(u'+43/512/12345678') - u'+43/512/12345678' - >>> v.to_python(u'0512/1234567-89') # 89 is the extension - u'+43/512/1234567-89' - >>> v.to_python(u'+43/512/1234567-89') - u'+43/512/1234567-89' - >>> v.to_python(u'0512 / 12345678') # Exception - >>> v.to_python(u'0512-12345678') # Exception - """ - # Inspired by formencode.national.InternationalPhoneNumber - - default_cc = 43 # Default country code - messages = {'phoneFormat': "'%%(value)s' is an invalid format. Please enter a number in the form +43/###/####### or 0###/########."} - - def to_python(self, value): - self.assert_string(value, None) - m = re.match(u'^(?:\+(\d+)/)?([\d/]+)(?:-(\d+))?$', value) - # This will separate - # u'+43/512/1234567-89' => (u'43', u'512/1234567', u'89') - # u'+43/512/1234/567-89' => (u'43', u'512/1234/567', u'89') - # u'+43/512/1234/567' => (u'43', u'512/1234/567', None) - # u'0512/1234567' => (None, u'0512/1234567', None) - if m is None: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None) - (country, phone, extension) = m.groups() - - # Phone - if phone.find(u'//') > -1 or phone.count('/') == 0: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None) - - # Country - if country is None: - if phone[0] != '0': raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None) - phone = phone[1:] - country = unicode(self.default_cc) - - if extension is None: return '+%s/%s' % (country, phone) - return '+%s/%s-%s' % (country, phone, extension) +DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')]) -# Deprecated -class AustrianPhoneNumberNone(NoneValidator): - def __init__(self): - NoneValidator.__init__(self, AustrianPhoneNumber()) +def difficulty_german_from_str(value): + return dictkey_from_str(value, DIFFICULTY_GERMAN) -# Deprecated -class AustrianPhoneNumberCommentLoop(NoneValidator): - def __init__(self): - NoneValidator.__init__(self, Loop(ValueComment(AustrianPhoneNumber()))) +def difficulty_german_to_str(value): + return dictkey_to_str(value, DIFFICULTY_GERMAN) -class GermanDifficulty(DictValidator): - """Converts the difficulty represented in a number from 1 to 3 (or None) - to a German representation: - u'' <=> None - u'leicht' <=> 1 - u'mittel' <=> 2 - u'schwer' <=> 3""" - def __init__(self): - DictValidator.__init__(self, {u'': None, u'leicht': 1, u'mittel': 2, u'schwer': 3}) +def opt_difficulty_german_from_str(value): + return opt_from_str(value, difficulty_german_from_str) -class GermanAvalanches(DictValidator): - """Converts the avalanches property represented as number from 1 to 4 (or None) - to a German representation: - u'' <=> None - u'kaum' <=> 1 - u'selten' <=> 2 - u'gelegentlich' <=> 3 - u'häufig' <=> 4""" - def __init__(self): - DictValidator.__init__(self, {u'': None, u'kaum': 1, u'selten': 2, u'gelegentlich': 3, u'häufig': 4}) - - -class GermanPublicTransport(DictValidator): - """Converts the public_transport property represented as number from 1 to 6 (or None) - to a German representation: - u'' <=> None - u'Sehr gut' <=> 1 - u'Gut' <=> 2 - u'Mittelmäßig' <=> 3 - u'Schlecht' <=> 4 - u'Nein' <=> 5 - u'Ja' <=> 6""" - def __init__(self): - DictValidator.__init__(self, {u'': None, u'Sehr gut': 1, u'Gut': 2, u'Mittelmäßig': 3, u'Schlecht': 4, u'Nein': 5, u'Ja': 6}) - - -class GermanTristateFloatComment(ValueComment): - """Converts the a property with the possible values 0.0, 0.5, 1.0 or None and an optional comment - in parenthesis to a German text: - u'' <=> (None, None) - u'Ja' <=> (1.0, None) - u'Teilweise' <=> (0.5, None) - u'Nein' <=> (0.0, None) - u'Ja (aber schmal)' <=> (1.0, u'aber schmal') - u'Teilweise (oben)' <=> (0.5, u'oben') - u'Nein (aber breit)' <=> (0.0, u'aber breit') - """ - def __init__(self): - ValueComment.__init__(self, GermanTristateFloat()) +def opt_difficulty_german_to_str(value): + return opt_to_str(value, difficulty_german_to_str) + + +opt_difficulty_german_converter = FromToConverter(opt_difficulty_german_from_str, opt_difficulty_german_to_str) + +AVALANCHES_GERMAN = OrderedDict([(1, 'kaum'), (2, 'selten'), (3, 'gelegentlich'), (4, 'häufig')]) -class UnsignedCommentNone(NoneValidator): - """Converts the a property with unsigned values an optional comment - in parenthesis to a text: - u'' <=> (None, None) - u'2 (Mo, Di)' <=> (2, u'Mo, Di') - u'7' <=> (7, None) - u'0' <=> (0, None) + +def avalanches_german_from_str(value): + return dictkey_from_str(value, AVALANCHES_GERMAN) + + +def avalanches_german_to_str(value): + return dictkey_to_str(value, AVALANCHES_GERMAN) + + +def opt_avalanches_german_from_str(value): + return opt_from_str(value, avalanches_german_from_str) + + +def opt_avalanches_german_to_str(value): + return opt_to_str(value, avalanches_german_to_str) + + +opt_avalanches_german_converter = FromToConverter(opt_avalanches_german_from_str, opt_avalanches_german_to_str) + + +PUBLIC_TRANSPORT_GERMAN = OrderedDict([(1, 'Sehr gut'), (2, 'Gut'), (3, 'Mittelmäßig'), (4, 'Schlecht'), (5, 'Nein'), (6, 'Ja')]) + + +def public_transport_german_from_str(value): + return dictkey_from_str(value, PUBLIC_TRANSPORT_GERMAN) + + +def public_transport_german_to_str(value): + return dictkey_to_str(value, PUBLIC_TRANSPORT_GERMAN) + + +def opt_public_transport_german_from_str(value): + return opt_from_str(value, public_transport_german_from_str) + + +def opt_public_transport_german_to_str(value): + return opt_to_str(value, public_transport_german_to_str) + + +opt_public_transport_german_converter = FromToConverter(opt_public_transport_german_from_str, opt_public_transport_german_to_str) + + +def value_comment_from_str(value, value_from_str=str_from_str, comment_from_str=str_from_str, comment_optional=False): + """Makes it possible to have a mandatory comment in parenthesis at the end of the string.""" + open_brackets = 0 + comment = '' + comment_end_pos = None + for i, char in enumerate(value[::-1]): + if char == ')': + open_brackets += 1 + if open_brackets == 1: + comment_end_pos = i + if len(value[-1-comment_end_pos:].rstrip()) > 1: + raise ValueError('invalid characters after comment') + elif char == '(': + open_brackets -= 1 + if open_brackets == 0: + comment = value[-i:-1-comment_end_pos] + value = value[:-i-1].rstrip() + break + else: + if open_brackets > 0: + raise ValueError('bracket mismatch') + if not comment_optional: + raise ValueError('mandatory comment not found') + return value_from_str(value), comment_from_str(comment) + + +def value_comment_to_str(value, value_to_str=str_to_str, comment_to_str=str_to_str, comment_optional=False): + left = value_to_str(value[0]) + comment = comment_to_str(value[1]) + if len(comment) > 0 or not comment_optional: + comment = '({})'.format(comment) + if len(left) == 0: + return comment + if len(comment) == 0: + return left + return '{} {}'.format(left, comment) + + +def opt_tristate_german_comment_from_str(value): + """Ja, Nein or Vielleicht, optionally with comment in parenthesis.""" + return value_comment_from_str(value, opt_tristate_german_from_str, opt_str_from_str, True) + + +def opt_tristate_german_comment_to_str(value): + return value_comment_to_str(value, opt_tristate_german_to_str, opt_str_to_str, True) + + +opt_tristate_german_comment_converter = FromToConverter(opt_tristate_german_comment_from_str, opt_tristate_german_comment_to_str) + + +def no_german_from_str(value, from_str=req_str_from_str, use_tuple=True, no_value=None): + if value == 'Nein': + return (False, no_value) if use_tuple else no_value + return (True, from_str(value)) if use_tuple else from_str(value) + + +def no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None): + if use_tuple: + if not value[0]: + return 'Nein' + return to_str(value[1]) + else: + if value == no_value: + return 'Nein' + return to_str(value) + + +def opt_no_german_from_str(value, from_str=str_from_str, use_tuple=True, no_value=None, none=(None, None)): + return opt_from_str(value, lambda v: no_german_from_str(v, from_str, use_tuple, no_value), none) + + +def opt_no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None, none=(None, None)): + return opt_to_str(value, lambda v: no_german_to_str(v, to_str, use_tuple, no_value), none) + + +def night_light_from_str(value): + """'Beleuchtungsanlage' Tristate with optional comment: + '' <=> (None, None) + 'Ja' <=> (1.0, None) + 'Teilweise' <=> (0.5, None) + 'Nein' <=> (0.0, None) + 'Ja (aber schmal)' <=> (1.0, 'aber schmal') + 'Teilweise (oben)' <=> (0.5, 'oben') + 'Nein (aber breit)' <=> (0.0, 'aber breit') """ - def __init__(self, max=None): - NoneValidator.__init__(self, ValueComment(Unsigned(max=max)), (None, None)) + return + + +def nightlightdays_from_str(value): + 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) + + +def nightlightdays_to_str(value): + return value_comment_to_str(value, lambda val: opt_to_str(val, int_to_str), opt_str_to_str, comment_optional=True) + + +nightlightdays_converter = FromToConverter(nightlightdays_from_str, nightlightdays_to_str) + +CACHET_REGEXP = [r'(Tiroler Naturrodelbahn-Gütesiegel) ([12]\d{3}) (leicht|mittel|schwer)$'] -class GermanCachet(formencode.FancyValidator): + +def single_cachet_german_from_str(value): + for pattern in CACHET_REGEXP: + match = re.match(pattern, value) + if match: + return match.groups() + raise ValueError("'{}' is no valid cachet".format(value)) + + +def single_cachet_german_to_str(value): + return ' '.join(value) + + +def cachet_german_from_str(value): """Converts a "Gütesiegel": - u'' <=> None - u'Nein' <=> 'Nein' - u'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' <=> u'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'""" - def to_python(self, value): - self.assert_string(value, None) - if value == u'': return None - elif value == u'Nein': return value - elif value.startswith(u'Tiroler Naturrodelbahn-Gütesiegel '): - p = value.split(" ") - Unsigned().to_python(p[2]) # check if year can be parsed - if not p[3] in ['leicht', 'mittel', 'schwer']: raise formencode.Invalid("Unbekannter Schwierigkeitsgrad", value, None) - return value - else: raise formencode.Invalid(u"Unbekanntes Gütesiegel", value, None) + '' => None + 'Nein' => [] + 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' => [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]""" + return opt_no_german_from_str(value, lambda val: enum_from_str(val, single_cachet_german_from_str), False, [], None) + - def from_python(self, value): - if value == None: return u'' - assert value != u'' - return self.to_python(self, value) +def cachet_german_to_str(value): + return opt_no_german_to_str(value, lambda val: enum_to_str(val, single_cachet_german_to_str), False, [], None) + + +cachet_german_converter = FromToConverter(cachet_german_from_str, cachet_german_to_str) + + +def url_from_str(value): + result = urllib.parse.urlparse(value) + if result.scheme not in ['http', 'https']: + raise ValueError('scheme has to be http or https') + if not result.netloc: + raise ValueError('url does not contain netloc') + return value + + +def url_to_str(value): + return value + + +def webauskunft_from_str(value): + return opt_no_german_from_str(value, url_from_str) + + +def webauskunft_to_str(value): + return opt_no_german_to_str(value, url_to_str) + + +webauskunft_converter = FromToConverter(webauskunft_from_str, webauskunft_to_str) class Url(formencode.FancyValidator): """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed.""" + # formencode 1.2.5 to formencode 1.3.0a1 sometimes raise ValueError instead of Invalid exceptions + # https://github.com/formencode/formencode/pull/61 urlv = formencode.validators.URL() - def to_python(self, value): - self.assert_string(value, None) + + def to_python(self, value, state=None): + self.assert_string(value, state) v = value - v = v.replace(u'ä', u'a') - v = v.replace(u'ö', u'o') - v = v.replace(u'ü', u'u') - v = v.replace(u'ß', u'ss') - v = self.urlv.to_python(v) + v = v.replace('ä', 'a') + v = v.replace('ö', 'o') + v = v.replace('ü', 'u') + v = v.replace('ß', 'ss') + v = self.urlv.to_python(v, state) return value - def from_python(self, value): + def from_python(self, value, state=None): return value -class UrlNeinNone(NoneValidator): - """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed. - The special value u"Nein" is allowed.""" - def __init__(self): - NoneValidator.__init__(self, NeinValidator(Url())) +def phone_number_from_str(value): + match = re.match(r'\+\d+(-\d+)*$', value) + if match is None: + raise ValueError('invalid format of phone number - use something like +43-699-1234567') + return value -class ValueCommentListNeinLoopNone(NoneValidator): - """Translates a semicolon separated list of values with optional comments in paranthesis or u'Nein' to itself. - An empty string is translated to None: - u'' <=> None - u'Nein' <=> u'Nein' - u'T-Mobile (gut); A1' <=> u'T-Mobile (gut); A1'""" - def __init__(self): - NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList()))) +def phone_number_to_str(value): + return value -class PhoneNumber(formencode.FancyValidator): - """Telefonnumber in international format, e.g. u'+43-699-1234567'""" - def __init__(self, default_cc=43): - self.validator = formencode.national.InternationalPhoneNumber(default_cc=lambda: default_cc) +def telefonauskunft_from_str(value): + 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) - def to_python(self, value): - return unicode(self.validator.to_python(value)) - def from_python(self, value): - return self.validator.from_python(value) +def telefonauskunft_to_str(value): + 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) -class PhoneCommentListNeinLoopNone(NoneValidator): - """List with semicolon-separated phone numbers in international format with optional comment or 'Nein' as string: - u'' <=> None - u'Nein' <=> u'Nein' - u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456' <=> u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456' - """ - def __init__(self, comments_are_optional): - NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(PhoneNumber(default_cc=43), comments_are_optional=comments_are_optional)))) +telefonauskunft_converter = FromToConverter(telefonauskunft_from_str, telefonauskunft_to_str) +def email_from_str(value): + """Takes an email address like 'office@example.com', checks it for correctness and returns it again as string.""" + try: + email.headerregistry.Address(addr_spec=value) + except email.errors.HeaderParseError as e: + raise ValueError('Invalid email address: {}'.format(value), e) + return value + + +def email_to_str(value): + return str(email) + + + + +class MaskedEmail(formencode.FancyValidator): + """A masked email address as defined here is an email address that has the `@` character replacted by the text `(at)`. + So instead of `abd.def@example.com` it would be `abc.def(at)example.com`. + 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 + as a bool indicating whether the email address was masked. + u'' <=> (None, None) + u'abc.def@example.com' <=> (u'abc.def@example.com', False) + u'abc.def(at)example.com' <=> (u'abc.def@example.com', True) + + """ + def __init__(self, *args, **kw): + if 'strip' not in kw: kw['strip'] = True + if 'not_empty' not in kw: kw['not_empty'] = False + if 'if_empty' not in kw: kw['if_empty'] = (None, None) + self.at = '(at)' + formencode.FancyValidator.__init__(self, *args, **kw) + + def _to_python(self, value, state=None): + email = value.replace(self.at, '@') + masked = value != email + val_email = formencode.validators.Email() + return val_email.to_python(email, state), masked + + def _from_python(self, value, state=None): + email, masked = value + if email is None: return '' + val_email = formencode.validators.Email() + email = val_email.from_python(email, state) + if masked: email = email.replace('@', self.at) + return email + + +''' class EmailCommentListNeinLoopNone(NoneValidator): """Converts a semicolon-separated list of email addresses with optional comments to itself. The special value of u'Nein' indicates that there are no email addresses. @@ -600,10 +653,13 @@ class EmailCommentListNeinLoopNone(NoneValidator): u'Nein' <=> u'Nein' u'first@example.com' <=> u'first@example.com' u'first@example.com (Nur Winter); second@example.com' <=> u'first@example.com (Nur Winter); second@example.com' - """ - def __init__(self): - NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(formencode.validators.Email())))) + If the parameter allow_masked_email is true, the following gives no error: + u'abc.def(at)example.com (comment)' <=> u'abc.def(at)example.com (comment)' + """ + def __init__(self, allow_masked_email=False): + NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(MaskedEmail() if allow_masked_email else formencode.validators.Email())))) +''' class WikiPage(formencode.FancyValidator): """Validates wiki page name like u'[[Birgitzer Alm]]'. @@ -611,13 +667,13 @@ class WikiPage(formencode.FancyValidator): An empty string is an error. u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]' """ - def to_python(self, value): - self.assert_string(value, None) + def to_python(self, value, state=None): + self.assert_string(value, state) if not value.startswith('[[') or not value.endswith(']]'): - raise formencode.Invalid('No valid wiki page name', value, None) + raise formencode.Invalid('No valid wiki page name', value, state) return value - def from_python(self, value): + def from_python(self, value, state=None): return value @@ -629,8 +685,10 @@ class WikiPageList(SemicolonList): """ def __init__(self): SemicolonList.__init__(self, WikiPage()) +''' +''' class WikiPageListLoopNone(NoneValidator): """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]' as string. u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> u'[[Birgitzer Alm]]; [[Kemater Alm]]' @@ -639,64 +697,178 @@ class WikiPageListLoopNone(NoneValidator): """ def __init__(self): NoneValidator.__init__(self, Loop(WikiPageList())) +''' -class TupleSecondValidator(formencode.FancyValidator): - """Does not really validate anything but puts the string through - a validator in the second part of a tuple. - Examples with an Unsigned() validator and the True argument: - u'6' <=> (True, 6) - u'2' <=> (True, 2)""" - def __init__(self, first=True, validator=UnicodeNone()): - self.first = first - self.validator = validator - - def to_python(self, value): - self.assert_string(value, None) - return self.first, self.validator.to_python(value) - - def from_python(self, value): - assert value[0] == self.first - return self.validator.from_python(value[1]) - - -class BoolUnicodeTupleValidator(NoneValidator): - """Translates an unparsed string or u'Nein' to a tuple: - u'' <=> (None, None) - u'Nein' <=> (False, None) - u'any text' <=> (True, u'any text') - """ - def __init__(self, validator=UnicodeNone()): - NoneValidator.__init__(self, NeinValidator(TupleSecondValidator(True, validator), (False, None)), (None, None)) +LIFT_GERMAN = ['Sessellift', 'Gondel', 'Linienbus', 'Taxi', 'Sonstige'] -class GermanLift(BoolUnicodeTupleValidator): +def lift_german_from_str(value): """Checks a lift_details property. It is a value comment property with the following values allowed: - u'Sessellift' - u'Gondel' - u'Linienbus' - u'Taxi' - u'Sonstige' + 'Sessellift' + 'Gondel' + 'Linienbus' + 'Taxi' + 'Sonstige' Alternatively, the value u'Nein' is allowed. An empty string maps to (None, None). - + Examples: - u'' <=> (None, None) - u'Nein' <=> (False, None) - u'Sessellift <=> (True, u'Sessellift') - u'Gondel (nur bis zur Hälfte)' <=> (True, u'Gondel (nur bis zur Hälfte)') - u'Sessellift; Taxi' <=> (True, u'Sessellift; Taxi') - u'Sessellift (Wochenende); Taxi (6 Euro)' <=> (True, u'Sessellift (Wochenende); Taxi (6 Euro)') + '' <=> None + 'Nein' <=> [] + 'Sessellift <=> [('Sessellift', None)] + 'Gondel (nur bis zur Hälfte)' <=> [('Gondel', 'nur bis zur Hälfte')] + 'Sessellift; Taxi' <=> [('Sessellift', None), ('Taxi', None)] + 'Sessellift (Wochenende); Taxi (6 Euro)' <=> [('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')] """ - def __init__(self): - BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList(DictValidator({u'Sessellift': u'Sessellift', u'Gondel': u'Gondel', u'Linienbus': u'Linienbus', u'Taxi': u'Taxi', u'Sonstige': u'Sonstige'})))) - + 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) -class SledRental(BoolUnicodeTupleValidator): - """The value can be an empty string, u'Nein' or a comma-separated list of unicode strings with optional comments. - u'' <=> (None, None) - u'Nein' <=> (False, None) - u'Talstation (nur mit Ticket); Schneealm' <=> (True, u'Talstation (nur mit Ticket); Schneealm')""" - def __init__(self): - BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList())) + +def lift_german_to_str(value): + 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) + + +lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str) + + +def sledrental_from_str(value): + """The value can be an empty string, 'Nein' or a semicolon-separated list of strings with optional comments. + '' => None + 'Nein' => [] + 'Talstation (nur mit Ticket); Schneealm' => [('Talstation', 'nur mit Ticket'), ('Schneealm', None)]""" + 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) + + +def sledrental_to_str(value): + 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) + + +sledrental_converter = FromToConverter(sledrental_from_str, sledrental_to_str) + + +class ValueErrorList(ValueError): + pass + + +def box_from_template(template, name, converter_dict): + if template.name.strip() != name: + raise ValueError('Box name has to be "{}"'.format(name)) + result = OrderedDict() + exceptions_dict = OrderedDict() + # check values + for key, converter in converter_dict.items(): + try: + if not template.has(key): + raise ValueError('Missing parameter "{}"'.format(key)) + result[key] = converter.from_str(str(template.get(key).value.strip())) + except ValueError as e: + exceptions_dict[key] = e + # check if keys are superfluous + superfluous_keys = {str(p.name.strip()) for p in template.params} - set(converter_dict.keys()) + for key in superfluous_keys: + exceptions_dict[key] = ValueError('Superfluous parameter: "{}"'.format(key)) + if len(exceptions_dict) > 0: + raise ValueErrorList('{} error(s) occurred when parsing template parameters.'.format(len(exceptions_dict)), exceptions_dict) + return result + + +def box_to_template(value, name, converter_dict): + template = mwparserfromhell.nodes.template.Template(name) + for key, converter in converter_dict.items(): + template.add(key, converter.to_str(value[key])) + return template + + +def template_from_str(value, name): + wikicode = mwparserfromhell.parse(value) + template_list = wikicode.filter_templates(name) + if len(name) == 0: + raise ValueError('No "{}" template was found'.format(name)) + if len(template_list) > 1: + raise ValueError('{} "{}" templates were found'.format(len(template_list), name)) + return template_list[0] + + +def box_from_str(value, name, converter_dict): + template = template_from_str(value, name) + return box_from_template(template, name, converter_dict) + + +def box_to_str(value, name, converter_dict): + return str(box_to_template(value, name, converter_dict)) + + +RODELBAHNBOX_TEMPLATE_NAME = 'Rodelbahnbox' + + +RODELBAHNBOX_DICT = OrderedDict([ + ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E' + ('Position oben', opt_lonlat_converter), # '47.583333 N 15.75 E' + ('Höhe oben', opt_meter_converter), # '2000' + ('Position unten', opt_lonlat_converter), # '47.583333 N 15.75 E' + ('Höhe unten', opt_meter_converter), # '1200' + ('Länge', opt_meter_converter), # 3500 + ('Schwierigkeit', opt_difficulty_german_converter), # 'mittel' + ('Lawinen', opt_avalanches_german_converter), # 'kaum' + ('Betreiber', opt_str_converter), # 'Max Mustermann' + ('Öffentliche Anreise', opt_public_transport_german_converter), # 'Mittelmäßig' + ('Aufstieg möglich', opt_bool_german_converter), # 'Ja' + ('Aufstieg getrennt', opt_tristate_german_comment_converter), # 'Ja' + ('Gehzeit', opt_minutes_converter), # 90 + ('Aufstiegshilfe', lift_german_converter), # 'Gondel (unterer Teil)' + ('Beleuchtungsanlage', opt_tristate_german_comment_converter), + ('Beleuchtungstage', nightlightdays_converter), # '3 (Montag, Mittwoch, Freitag)' + ('Rodelverleih', sledrental_converter), # 'Talstation Serlesbahnan' + ('Gütesiegel', cachet_german_converter), # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' + ('Webauskunft', webauskunft_converter), # 'http://www.nösslachhütte.at/page9.php' + ('Telefonauskunft', telefonauskunft_converter), # '+43-664-5487520 (Mitterer Alm)' + ('Bild', opt_str_converter), + ('In Übersichtskarte', opt_bool_german_converter), + ('Forumid', opt_int_converter) +]) + + +def rodelbahnbox_from_template(template): + return box_from_template(template, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT) + + +def rodelbahnbox_to_template(value): + return box_to_template(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT) + + +def rodelbahnbox_from_str(value): + return box_from_str(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT) + + +def rodelbahnbox_to_str(value): + template = rodelbahnbox_to_template(value) + template_to_table(template, 20) + return str(template) + + +GASTHAUSBOX_TEMPLATE_NAME = 'Gasthausbox' + + +''' +GASTHAUSBOX_DICT = OrderedDict([ + ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E' + ('Höhe', opt_meter_converter), + ('Betreiber', opt_str_converter), + ('Sitzplätze', opt_int_converter), + ('Übernachtung', BoolUnicodeTupleValidator()), + ('Rauchfrei', opt_tristate_german_validator), + ('Rodelverleih', BoolUnicodeTupleValidator()), + ('Handyempfang', ValueCommentListNeinLoopNone()), + ('Homepage', webauskunft_converter), + ('E-Mail', EmailCommentListNeinLoopNone(allow_masked_email=True)), + ('Telefon', PhoneCommentListNeinLoopNone(comments_are_optional=True)), + ('Bild', opt_str_converter), + ('Rodelbahnen', WikiPageListLoopNone())]) +''' + + +def sledrun_page_title_to_pretty_url(page_title): + """Converts a page_title from the page_title column of wrsledruncache to name_url. + name_url is not used by MediaWiki but by new applications like wrweb.""" + return page_title.lower().replace(' ', '-').replace('_', '-').replace('(', '').replace(')', '')