Copied the validators from wradmin.
authorphilipp <philipp@7aebc617-e5e2-0310-91dc-80fb5f6d2477>
Mon, 31 Jan 2011 20:27:33 +0000 (20:27 +0000)
committerphilipp <philipp@7aebc617-e5e2-0310-91dc-80fb5f6d2477>
Mon, 31 Jan 2011 20:27:33 +0000 (20:27 +0000)
git-svn-id: http://www.winterrodeln.org/svn/servermediawiki/trunk/wrpylib@753 7aebc617-e5e2-0310-91dc-80fb5f6d2477

13 files changed:
setup.py
src/__init__.py [deleted file]
src/mwapi.py [deleted file]
src/mwmarkup.py [deleted file]
src/wrmwmarkup.py [deleted file]
src/wrvalidators.py [deleted file]
tests/__init__.py [new file with mode: 0644]
tests/test_wrvalidators.py [new file with mode: 0644]
wrpylib/__init__.py [new file with mode: 0644]
wrpylib/mwapi.py [new file with mode: 0644]
wrpylib/mwmarkup.py [new file with mode: 0644]
wrpylib/wrmwmarkup.py [new file with mode: 0644]
wrpylib/wrvalidators.py [new file with mode: 0644]

index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..51c76fd20acd812704ba46e6ede663d8ab8cb758 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -0,0 +1,12 @@
+#!/usr/bin/python2.6
+# -*- coding: iso-8859-15 -*-
+from distutils.core import setup
+
+setup(name='wrpylib',
+    version='0.0.0',
+    description='Winterrodeln Python Library',
+    author='Philipp Spitzer',
+    author_email='philipp.spitzer@winterrodeln.org',
+    packages=['wrpylib']
+)
+
diff --git a/src/__init__.py b/src/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/mwapi.py b/src/mwapi.py
deleted file mode 100644 (file)
index 5e23fec..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/python2.6
-# -*- coding: iso-8859-15 -*-
-# $Id$
-# $HeadURL$
-"""This module contains general functions for using the MediaWiki API. There is one very
-good Python wrapper implementation of the MediaWiki API:
-* wikitools http://code.google.com/p/python-wikitools/
-
-Therefore this module doesn't need to have much content.
-"""
-
-
diff --git a/src/mwmarkup.py b/src/mwmarkup.py
deleted file mode 100644 (file)
index 11d1596..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/python2.6
-# -*- coding: iso-8859-15 -*-
-# $Id$
-# $HeadURL$
-"""This module contains general functions that help parsing the mediawiki markup.
-I looked for an already existing MediaWiki parser in Python but I didn't find anything 
-that convinced me. However, here are the links:
-
-* py-wikimarkup https://github.com/dcramer/py-wikimarkup
-* mwlib http://code.pediapress.com/wiki/wiki
-"""
-
-
diff --git a/src/wrmwmarkup.py b/src/wrmwmarkup.py
deleted file mode 100644 (file)
index 26c833b..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/python2.6
-# -*- coding: iso-8859-15 -*-
-# $Id$
-# $HeadURL$
-"""This module contains winterrodeln specific functions that are prcocessing the MediaWiki markup.
-"""
-
diff --git a/src/wrvalidators.py b/src/wrvalidators.py
deleted file mode 100644 (file)
index 84e9680..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/python2.6
-# -*- coding: iso-8859-15 -*-
-# $Id$
-# $HeadURL$
-"""This module contains general functions for using the MediaWiki API. There is one very
-good Python wrapper implementation of the MediaWiki API:
-* wikitools http://code.google.com/p/python-wikitools/
-
-Therefore this module doesn't need to have much content.
-"""
-
-
-
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_wrvalidators.py b/tests/test_wrvalidators.py
new file mode 100644 (file)
index 0000000..087d791
--- /dev/null
@@ -0,0 +1,147 @@
+#!/usr/bin/python2.6
+# -*- coding: iso-8859-15 -*-
+import wrpylib.wrvalidators
+import formencode
+
+
+def test_NoneValidator():
+    v =  wrpylib.wrvalidators.NoneValidator(wrpylib.wrvalidators.Unicode())
+    assert v.to_python(u'') == None
+    assert v.from_python(None) == u''
+
+
+def test_UnsignedNone():
+    v = wrpylib.wrvalidators.UnsignedNone()
+    assert v.to_python(u'42') == 42
+    assert v.to_python(u'') == None
+    assert v.from_python(42) == u'42'
+    assert v.from_python(None) == u''
+
+
+def test_GeoNone():
+    coord = u'47.076207 N 11.453553 E'
+    v = wrpylib.wrvalidators.GeoNone()
+    (lat, lon) = v.to_python(coord)
+    assert lat == 47.076207
+    assert lon == 11.453553
+    assert v.to_python(u'') == (None, None)
+
+    assert v.from_python((lat, lon)) == coord
+    assert v.from_python((None, None)) == u''
+
+
+def test_GermanTristateTuple():
+    v = wrpylib.wrvalidators.GermanTristateTuple()
+    assert v.to_python(u'') == (None, None)
+    assert v.to_python(u'Ja') == (True, False)
+    assert v.to_python(u'Nein') == (False, True)
+    assert v.to_python(u'Teilweise') == (True, True)
+    assert v.from_python((None, None)) == u''
+    assert v.from_python((False, True)) == u'Nein'
+    assert v.from_python((True, False)) == u'Ja'
+    assert v.from_python((True, True)) == u'Teilweise'
+
+
+def tes_GermanTristateFloat():
+    v = wrpylib.wrvalidators.GermanTristateFloat()
+    assert v.to_python(u'') == None
+    assert v.to_python(u'Ja') == 1.0
+    assert v.to_python(u'Nein') == 0.0
+    assert v.to_python(u'Teilweise') == 0.5
+    assert v.from_python(None) == u''
+    assert v.from_python(0.0) == u'Nein'
+    assert v.from_python(1.0) == u'Ja'
+    assert v.from_python(0.5) == u'Teilweise'
+
+
+def test_ValueCommentList():
+    v = wrpylib.wrvalidators.ValueCommentList()
+    assert v.to_python(u'abc') == [(u'abc', None)]
+    assert v.to_python(u'abc def') == [(u'abc def', None)]
+    assert v.to_python(u'value (comment)') == [(u'value', u'comment')]
+    assert v.to_python(u'value (comment)') == [(u'value', u'comment')]
+    assert v.to_python(u'value1 (comment); value2') == [(u'value1', u'comment'), (u'value2', None)]
+    assert v.to_python(u'value1 (comment1); value2; value3 (comment3)') == [(u'value1', u'comment1'), (u'value2', None), ('value3', 'comment3')]
+    assert v.to_python(u'value1 (comment1); value2 (test (not easy))') == [(u'value1', u'comment1'), (u'value2', u'test (not easy)')]
+
+
+def test_UrlNeinNone():
+    v = wrpylib.wrvalidators.UrlNeinNone()
+    assert v.to_python(u'') == None
+    assert v.to_python(u'Nein') == u'Nein'
+    assert v.to_python(u'http://www.höttingeralm.at') == u'http://www.höttingeralm.at'
+    assert v.from_python(None) == u''
+    assert v.from_python(u'Nein') == u'Nein'
+    assert v.from_python(u'http://www.höttingeralm.at') == u'http://www.höttingeralm.at'
+
+
+def test_ValueCommentListNeinLoopNone():
+    v = wrpylib.wrvalidators.ValueCommentListNeinLoopNone()
+    assert v.to_python(u'') == None
+    assert v.to_python(u'Nein') == u'Nein'
+    assert v.to_python(u'T-Mobile (gut); A1') == u'T-Mobile (gut); A1'
+    assert v.from_python(None) == u''
+    assert v.from_python(u'Nein') == u'Nein'
+    assert v.from_python(u'T-Mobile (gut); A1') == u'T-Mobile (gut); A1'
+    
+
+def test_PhoneCommentListNeinLoopNone():
+    v = wrpylib.wrvalidators.PhoneCommentListNeinLoopNone(comments_are_optional=True)
+    assert v.to_python(u'') == None
+    assert v.to_python(u'Nein') == u'Nein'
+    assert v.to_python(u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456') == u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456'
+    assert v.from_python(None) == u''
+    assert v.from_python(u'Nein') == u'Nein'
+    assert v.from_python(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 test_EmailCommentListNeinLoopNone():
+    v = wrpylib.wrvalidators.EmailCommentListNeinLoopNone()
+    assert v.to_python(u'') == None
+    assert v.to_python(u'Nein') == u'Nein'
+    assert v.to_python(u'first@example.com') == u'first@example.com'
+    assert v.to_python(u'first@example.com (Nur Winter); second@example.com') == u'first@example.com (Nur Winter); second@example.com'
+    assert v.from_python(None) == u''
+    assert v.from_python(u'Nein') == u'Nein'
+    assert v.from_python(u'first@example.com') == u'first@example.com'
+    assert v.from_python(u'first@example.com (Nur Winter); second@example.com') == u'first@example.com (Nur Winter); second@example.com'
+
+
+def test_GermanLift():
+    v = wrpylib.wrvalidators.GermanLift()
+    assert v.to_python(u'') == (None, None)
+    assert v.to_python(u'Nein') == (False, None)
+    assert v.to_python(u'Sessellift (4 Euro)') == (True, u'Sessellift (4 Euro)')
+    assert v.from_python((None, None)) == u''
+    assert v.from_python((False, None)) == u'Nein'
+    assert v.from_python((True, u'Sessellift (4 Euro)')) == u'Sessellift (4 Euro)'
+
+
+def test_WikiPageListLoopNone():
+    v = wrpylib.wrvalidators.WikiPageListLoopNone()
+    assert v.to_python(u'') == None
+    assert v.to_python(u'[[Birgitzer Alm]]; [[Kemater Alm]]') == u'[[Birgitzer Alm]]; [[Kemater Alm]]'
+    assert v.from_python(None) == u''
+    assert v.from_python(u'[[Birgitzer Alm]]; [[Kemater Alm]]') == u'[[Birgitzer Alm]]; [[Kemater Alm]]'
+
+
+def test_BoolUnicodeTupleValidator():
+    v = wrpylib.wrvalidators.BoolUnicodeTupleValidator()
+    assert v.to_python(u'') == (None, None)
+    assert v.to_python(u'Nein') == (False, None)
+    assert v.to_python(u'any text') == (True, u'any text')
+    assert v.from_python((None, None)) == u''
+    assert v.from_python((False, None)) == u'Nein'
+    assert v.from_python((True, u'any text')) == u'any text'
+
+
+def test_SledRental():
+    v = wrpylib.wrvalidators.SledRental()
+    assert v.to_python(u'') == (None, None)
+    assert v.to_python(u'Nein') == (False, None)
+    assert v.to_python(u'Ja') == (True, u'Ja')
+    assert v.to_python(u'Talstation (nur mit Ticket); Schneealm') == (True, u'Talstation (nur mit Ticket); Schneealm')
+    assert v.from_python((None, None)) == u''
+    assert v.from_python((False, None)) == u'Nein'
+    assert v.from_python((True, u'Talstation (nur mit Ticket); Schneealm')) == u'Talstation (nur mit Ticket); Schneealm'
+    assert v.from_python((True, u'Ja')) == u'Ja'
diff --git a/wrpylib/__init__.py b/wrpylib/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/wrpylib/mwapi.py b/wrpylib/mwapi.py
new file mode 100644 (file)
index 0000000..5e23fec
--- /dev/null
@@ -0,0 +1,12 @@
+#!/usr/bin/python2.6
+# -*- coding: iso-8859-15 -*-
+# $Id$
+# $HeadURL$
+"""This module contains general functions for using the MediaWiki API. There is one very
+good Python wrapper implementation of the MediaWiki API:
+* wikitools http://code.google.com/p/python-wikitools/
+
+Therefore this module doesn't need to have much content.
+"""
+
+
diff --git a/wrpylib/mwmarkup.py b/wrpylib/mwmarkup.py
new file mode 100644 (file)
index 0000000..11d1596
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/python2.6
+# -*- coding: iso-8859-15 -*-
+# $Id$
+# $HeadURL$
+"""This module contains general functions that help parsing the mediawiki markup.
+I looked for an already existing MediaWiki parser in Python but I didn't find anything 
+that convinced me. However, here are the links:
+
+* py-wikimarkup https://github.com/dcramer/py-wikimarkup
+* mwlib http://code.pediapress.com/wiki/wiki
+"""
+
+
diff --git a/wrpylib/wrmwmarkup.py b/wrpylib/wrmwmarkup.py
new file mode 100644 (file)
index 0000000..26c833b
--- /dev/null
@@ -0,0 +1,7 @@
+#!/usr/bin/python2.6
+# -*- coding: iso-8859-15 -*-
+# $Id$
+# $HeadURL$
+"""This module contains winterrodeln specific functions that are prcocessing the MediaWiki markup.
+"""
+
diff --git a/wrpylib/wrvalidators.py b/wrpylib/wrvalidators.py
new file mode 100644 (file)
index 0000000..831f84c
--- /dev/null
@@ -0,0 +1,702 @@
+#!/usr/bin/python2.6
+# -*- 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.
+"""
+import formencode
+import formencode.national
+import datetime
+import re
+import xml.dom.minidom as minidom
+from xml.parsers.expat import ExpatError
+
+
+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)
+
+
+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)
+
+    def from_python(self, value):
+        return unicode(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())
+
+
+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)
+
+
+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))
+
+
+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())
+
+
+class Loop(formencode.FancyValidator):
+    """Takes a validator and calls from_python(to_python(value))."""
+    def __init__(self, validator):
+        self.validator = validator
+
+    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)
+
+
+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)
+
+
+class DateTimeNoSec(GenericDateTime):
+    def __init__(self, **keywords):
+        GenericDateTime.__init__(self, '%Y-%m-%d %H:%M', **keywords)
+
+
+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'))
+
+
+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)
+
+
+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):
+    "Formats multiple coordinates, even in multiple lines to [(latitude, longitude, elevation), ...] or [(latitude, longitude, None), ...] tuplets."
+    
+    # Valid for input_format
+    FORMAT_GUESS = 0         # guesses the input format; default for input_format
+    FORMAT_NONE = -1          # indicates missing formats
+    
+    # Valid for input_format and output_format
+    FORMAT_GEOCACHING = 1    # e.g. "N 47° 13.692 E 011° 25.535"
+    FORMAT_WINTERRODELN = 2  # e.g. "47.222134 N 11.467211 E"
+    FORMAT_GMAPPLUGIN = 3    # e.g. "47.232922, 11.452239"
+    FORMAT_GPX = 4           # e.g. "<trkpt lat="47.181289" lon="11.408827"><ele>1090.57</ele></trkpt>"
+    
+    input_format = FORMAT_GUESS
+    output_format = FORMAT_WINTERRODELN
+    last_input_format = FORMAT_NONE
+
+    def __init__(self, input_format = FORMAT_GUESS, output_format = FORMAT_WINTERRODELN, **keywords):
+        self.input_format = input_format
+        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)
+        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?
+        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)
+                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))
+                    last_input_format = self.FORMAT_WINTERRODELN
+                    continue
+                    
+            if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_WINTERRODELN:
+                r = re.match(u'(\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)
+                if not r is None:
+                    result.append((float(r.groups()[0]), float(r.groups()[1]), None))
+                    last_input_format = self.FORMAT_GMAPPLUGIN
+                    continue
+                
+            if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GPX:
+                try:
+                    xml = minidom.parseString(line)
+                    coord = xml.documentElement
+                    lat = float(coord.getAttribute('lat'))
+                    lon = float(coord.getAttribute('lon'))
+                    try: ele = float(coord.childNodes[0].childNodes[0].nodeValue)
+                    except (IndexError, ValueError): ele = None
+                    result.append((lat, lon, ele))
+                    last_input_format = self.FORMAT_GPX
+                    continue
+                except (ExpatError, IndexError, ValueError): pass
+
+            raise formencode.Invalid(u"Coordinates '%s' have no known format" % line, value, None)
+            
+        return result
+    
+    def from_python(self, value):
+        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))
+                
+            elif output_format == self.FORMAT_WINTERRODELN:
+                result.append(u'%.6f N %.6f E' % (latitude, longitude))
+
+            elif output_format == self.FORMAT_GMAPPLUGIN:
+                result.append(u'%.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))
+            
+            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?
+            
+        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)
+
+
+# Deprecated
+class AustrianPhoneNumberNone(NoneValidator):
+    def __init__(self):
+        NoneValidator.__init__(self, AustrianPhoneNumber())
+
+
+# Deprecated
+class AustrianPhoneNumberCommentLoop(NoneValidator):
+    def __init__(self):
+        NoneValidator.__init__(self, Loop(ValueComment(AustrianPhoneNumber())))
+
+
+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})
+
+
+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())
+
+
+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 __init__(self, max=None):
+        NoneValidator.__init__(self, ValueComment(Unsigned(max=max)), (None, None))
+
+
+class GermanCachet(formencode.FancyValidator):
+    """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)
+    
+    def from_python(self, value):
+        if value == None: return u''
+        assert value != u''
+        return self.to_python(self, value)
+
+
+class Url(formencode.FancyValidator):
+    """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed."""
+    urlv = formencode.validators.URL()    
+    def to_python(self, value):
+        self.assert_string(value, None)
+        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)
+        return value
+    
+    def from_python(self, value):
+        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()))
+
+
+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())))
+
+
+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 to_python(self, value):
+        return unicode(self.validator.to_python(value))
+
+    def from_python(self, value):
+        return self.validator.from_python(value)
+
+
+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))))
+
+
+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.
+    The empty string translates to None:
+    u''                                                   <=> None
+    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()))))
+
+
+class WikiPage(formencode.FancyValidator):
+    """Validates wiki page name like u'[[Birgitzer Alm]]'.
+    The page is not checked for existance.
+    An empty string is an error.
+    u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
+    """
+    def to_python(self, value):
+        self.assert_string(value, None)
+        if not value.startswith('[[') or not value.endswith(']]'): 
+            raise formencode.Invalid('No valid wiki page name', value, None)
+        return value
+    
+    def from_python(self, value):
+        return value
+
+
+class WikiPageList(SemicolonList):
+    """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]'.
+    u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> [u'[[Birgitzer Alm]]', u'[[Kemater Alm]]']
+    u'[[Birgitzer Alm]]'                  <=> [u'[[Birgitzer Alm]]']
+    u''                                   <=> []
+    """
+    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]]'
+    u'[[Birgitzer Alm]]'                  <=> u'[[Birgitzer Alm]]'
+    u''                                   <=> None
+    """
+    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))
+
+
+class GermanLift(BoolUnicodeTupleValidator):
+    """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'
+    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)')
+    """
+    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'}))))
+        
+
+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()))