Removed unused converters.
[philipp/winterrodeln/wrpylib.git] / wrpylib / wrvalidators.py
index 831f84ca3105b120bb6eee3c524cdb2b0185a3a7..0a180882f466b270fdf144534da68e2d354649ba 100644 (file)
-#!/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 '<latitude> N <longitude> 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'<trkpt lat="%.6f" lon="%.6f"><ele>%.2f</ele></trkpt>' % (latitude, longitude, height))
-                else: result.append(u'<trkpt lat="%.6f" lon="%.6f"/>' % (latitude, longitude))
+                if not height is None: result.append('<trkpt lat="%.6f" lon="%.6f"><ele>%.2f</ele></trkpt>' % (latitude, longitude, height))
+                else: result.append('<trkpt lat="%.6f" lon="%.6f"/>' % (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(')', '')