Renamed RodelbahnboxValidator to RodelbahnboxDictValidator.
[philipp/winterrodeln/wrpylib.git] / wrpylib / wrvalidators.py
index 825050e15b9a2073d83547ea78ac24c9333ccf84..beea7205929e085f3a25039c0d7ad96c816801e0 100644 (file)
@@ -1,4 +1,4 @@
-#!/usr/bin/python2.6
+#!/usr/bin/python2.7
 # -*- coding: iso-8859-15 -*-
 # $Id$
 # $HeadURL$
@@ -6,12 +6,12 @@
 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
+import formencode
+import formencode.national
 
 
 class NoneValidator(formencode.FancyValidator):
@@ -20,14 +20,14 @@ class NoneValidator(formencode.FancyValidator):
         self.validator = validator
         self.python_none = python_none
     
-    def to_python(self, value):
-        self.assert_string(value, None)
+    def to_python(self, value, state=None):
+        self.assert_string(value, state)
         if value == u'': return self.python_none
-        return self.validator.to_python(value)
+        return self.validator.to_python(value, state)
     
-    def from_python(self, value):
+    def from_python(self, value, state=None):
         if value == self.python_none: return u''
-        return self.validator.from_python(value)
+        return self.validator.from_python(value, state)
 
 
 class NeinValidator(formencode.FancyValidator):
@@ -46,24 +46,24 @@ class NeinValidator(formencode.FancyValidator):
         self.validator = validator
         self.python_no = python_no
     
-    def to_python(self, value):
-        self.assert_string(value, None)
+    def to_python(self, value, state=None):
+        self.assert_string(value, state)
         if value == u'Nein': return self.python_no
-        return self.validator.to_python(value)
+        return self.validator.to_python(value, state)
     
-    def from_python(self, value):
+    def from_python(self, value, state=None):
         if value == self.python_no: return u'Nein'
-        return self.validator.from_python(value)
+        return self.validator.from_python(value, state)
 
 
 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)
+    def to_python(self, value, state=None):
+        self.assert_string(value, state)
         return unicode(value)
 
-    def from_python(self, value):
+    def from_python(self, value, state=None):
         return unicode(value)
 
 
@@ -84,11 +84,11 @@ class Unsigned(formencode.FancyValidator):
     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 to_python(self, value, state=None):
+        self.assert_string(value, state)
+        return self.iv.to_python(value, state)
     
-    def from_python(self, value):
+    def from_python(self, value, state=None):
         return unicode(value)
 
 
@@ -120,15 +120,15 @@ class Loop(formencode.FancyValidator):
     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 to_python(self, value, state=None):
+        self.assert_string(value, state)
+        return self.validator.from_python(self.validator.to_python(value, state))
     
-    def from_python(self, value):
+    def from_python(self, value, state=None):
         # we don't call self.validator.to_python(self.validator.from_python(value))
         # here because our to_python implementation basically leaves the input untouched
         # and so should from_python do.
-        return self.validator.from_python(self.validator.to_python(value))
+        return self.validator.from_python(self.validator.to_python(value, state))
 
 
 class DictValidator(formencode.FancyValidator):
@@ -142,15 +142,16 @@ class DictValidator(formencode.FancyValidator):
     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)
+    def to_python(self, value, state=None):
+        self.assert_string(value, state)
+        if not self.dict.has_key(value): raise formencode.Invalid("Key not found in dict.", value, state)
         return self.dict[value]
     
-    def from_python(self, value):
+    def from_python(self, value, state=None):
         for k, v in self.dict.iteritems():
-            if type(v) == type(value) and v == value: return k
-        raise formencode.Invalid('Invalid value', value, None)
+            if v == value:
+                return k
+        raise formencode.Invalid('Invalid value', value, state)
 
 
 class GermanBoolNone(DictValidator):
@@ -198,28 +199,28 @@ class ValueComment(formencode.FancyValidator):
         self.comment_validator = comment_validator
         self.comment_is_optional = comment_is_optional
     
-    def to_python(self, value):
-        self.assert_string(value, None)
+    def to_python(self, value, state=None):
+        self.assert_string(value, state)
         if value == u'':
             v = value
             c = value
         else:
             right = value.rfind(')')
             if right+1 != len(value):
-                if not self.comment_is_optional: raise formencode.Invalid(u'Mandatory comment not present', value, None)
+                if not self.comment_is_optional: raise formencode.Invalid(u'Mandatory comment not present', value, state)
                 v = value
                 c = u''
             else:
                 left = value.rfind('(')
-                if left < 0: raise formencode.Invalid(u'Invalid format', value, None)
+                if left < 0: raise formencode.Invalid(u'Invalid format', value, state)
                 v = value[:left].strip()
                 c = value[left+1:right].strip()
-        return self.value_validator.to_python(v), self.comment_validator.to_python(c)
+        return self.value_validator.to_python(v, state), self.comment_validator.to_python(c, state)
 
-    def from_python(self, value):
+    def from_python(self, value, state=None):
         assert len(value) == 2
-        v = self.value_validator.from_python(value[0])
-        c = self.comment_validator.from_python(value[1])
+        v = self.value_validator.from_python(value[0], state)
+        c = self.comment_validator.from_python(value[1], state)
         if len(c) > 0:
             if len(v) > 0: return u'%s (%s)' % (v, c)
             else: return u'(%s)' % c
@@ -232,12 +233,12 @@ class SemicolonList(formencode.FancyValidator):
     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 to_python(self, value, state=None):
+        self.assert_string(value, state)
+        return [self.validator.to_python(s.strip(), state) for s in value.split(';')]
     
-    def from_python(self, value):
-        return "; ".join([self.validator.from_python(s) for s in value])
+    def from_python(self, value, state=None):
+        return "; ".join([self.validator.from_python(s, state) for s in value])
         
     
 class ValueCommentList(SemicolonList):
@@ -265,9 +266,9 @@ class GenericDateTime(formencode.FancyValidator):
         self.date_time_format = date_time_format
     
     def to_python(self, value, state=None):
-        self.assert_string(value, None)
+        self.assert_string(value, state)
         try: return datetime.datetime.strptime(value, self.date_time_format)
-        except ValueError, e: raise formencode.Invalid(str(e), value, None)
+        except ValueError, e: raise formencode.Invalid(str(e), value, state)
     
     def from_python(self, value, state=None):
         return value.strftime(self.date_time_format)
@@ -286,13 +287,13 @@ class DateNone(NoneValidator):
 
 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)
+    def to_python(self, value, state=None):
+        self.assert_string(value, state)
         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)
+        if r is None: raise formencode.Invalid(u"Coordinates '%s' have not a format like '47.076207 N 11.453553 E'" % value, value, state)
         return (float(r.groups()[0]), float(r.groups()[1]))
     
-    def from_python(self, value):
+    def from_python(self, value, state=None):
         latitude, longitude = value
         return u'%.6f N %.6f E' % (latitude, longitude)
 
@@ -325,11 +326,11 @@ 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(u"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 = []
@@ -369,11 +370,11 @@ 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(u"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:
@@ -392,7 +393,7 @@ class MultiGeo(formencode.FancyValidator):
                 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?
+                raise formencode.Invalid(u"output_format %d is not recognized" % output_format, value, state) # Shouldn't it be an other type of runtime error?
             
         return "\n".join(result)
 
@@ -420,23 +421,23 @@ class AustrianPhoneNumber(formencode.FancyValidator):
     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)
+    def to_python(self, value, state=None):
+        self.assert_string(value, state)
         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)
+        if m is None: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, state)
         (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)
+        if phone.find(u'//') > -1 or phone.count('/') == 0: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, state)
         
         # Country
         if country is None:
-            if phone[0] != '0': raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None)
+            if phone[0] != '0': raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, state)
             phone = phone[1:]
             country = unicode(self.default_cc)
         
@@ -525,37 +526,40 @@ class GermanCachet(formencode.FancyValidator):
     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)
+    def to_python(self, value, state=None):
+        self.assert_string(value, state)
         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)
+            Unsigned().to_python(p[2], state) # check if year can be parsed
+            if not p[3] in ['leicht', 'mittel', 'schwer']: raise formencode.Invalid("Unbekannter Schwierigkeitsgrad", value, state)
             return value
-        else: raise formencode.Invalid(u"Unbekanntes Gütesiegel", value, None)
+        else: raise formencode.Invalid(u"Unbekanntes Gütesiegel", value, state)
     
-    def from_python(self, value):
-        if value == None: return u''
+    def from_python(self, value, state=None):
+        if value is None: return u''
         assert value != u''
-        return self.to_python(value)
+        return self.to_python(value, state)
 
 
 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 = self.urlv.to_python(v, state)
         return value
     
-    def from_python(self, value):
+    def from_python(self, value, state=None):
         return value
 
 
@@ -581,11 +585,11 @@ class PhoneNumber(formencode.FancyValidator):
     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 to_python(self, value, state=None):
+        return unicode(self.validator.to_python(value, state))
 
-    def from_python(self, value):
-        return self.validator.from_python(value)
+    def from_python(self, value, state=None):
+        return self.validator.from_python(value, state)
 
 
 class PhoneCommentListNeinLoopNone(NoneValidator):
@@ -598,6 +602,38 @@ class PhoneCommentListNeinLoopNone(NoneValidator):
         NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(PhoneNumber(default_cc=43), comments_are_optional=comments_are_optional))))
 
 
+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 not kw.has_key('strip'): kw['strip'] = True
+        if not kw.has_key('not_empty'): kw['not_empty'] = False
+        if not kw.has_key('if_empty'): 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 u''
+        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.
@@ -606,9 +642,12 @@ 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'
+
+    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):
-        NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(formencode.validators.Email()))))
+    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):
@@ -617,13 +656,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
 
 
@@ -657,13 +696,13 @@ class TupleSecondValidator(formencode.FancyValidator):
         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 to_python(self, value, state=None):
+        self.assert_string(value, state)
+        return self.first, self.validator.to_python(value, state)
     
-    def from_python(self, value):
+    def from_python(self, value, state=None):
         assert value[0] == self.first
-        return self.validator.from_python(value[1])
+        return self.validator.from_python(value[1], state)
 
 
 class BoolUnicodeTupleValidator(NoneValidator):
@@ -706,3 +745,31 @@ class SledRental(BoolUnicodeTupleValidator):
     u'Talstation (nur mit Ticket); Schneealm' <=> (True, u'Talstation (nur mit Ticket); Schneealm')"""
     def __init__(self):
         BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList()))
+
+
+class RodelbahnboxDictValidator(formencode.Schema):
+    """Takes the fields of the Rodelbahnbox as dict of strings and returns them as dict of appropriet types."""
+    def __init__(self):
+        self.add_field(u'Position', GeoNone()) # '47.583333 N 15.75 E'
+        self.add_field(u'Position oben', GeoNone()) # '47.583333 N 15.75 E'
+        self.add_field(u'Höhe oben', UnsignedNone()) # '2000'
+        self.add_field(u'Position unten', GeoNone()) # '47.583333 N 15.75 E'
+        self.add_field(u'Höhe unten', UnsignedNone()) # '1200'
+        self.add_field(u'Länge', UnsignedNone()) # 3500
+        self.add_field(u'Schwierigkeit', GermanDifficulty()) # 'mittel'
+        self.add_field(u'Lawinen', GermanAvalanches()) # 'kaum'
+        self.add_field(u'Betreiber', UnicodeNone()) # 'Max Mustermann'
+        self.add_field(u'Öffentliche Anreise', GermanPublicTransport()) # 'Mittelmäßig'
+        self.add_field(u'Aufstieg möglich', GermanBoolNone()) # 'Ja'
+        self.add_field(u'Aufstieg getrennt', GermanTristateFloatComment()) # 'Ja'
+        self.add_field(u'Gehzeit', UnsignedNone()) # 90
+        self.add_field(u'Aufstiegshilfe', GermanLift()) # 'Gondel (unterer Teil)'
+        self.add_field(u'Beleuchtungsanlage', GermanTristateFloatComment())
+        self.add_field(u'Beleuchtungstage', UnsignedCommentNone(7)) # '3 (Montag, Mittwoch, Freitag)'
+        self.add_field(u'Rodelverleih', SledRental()) # 'Talstation Serlesbahnan'
+        self.add_field(u'Gütesiegel', GermanCachet()) # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
+        self.add_field(u'Webauskunft', UrlNeinNone()) # 'http://www.nösslachhütte.at/page9.php'
+        self.add_field(u'Telefonauskunft', PhoneCommentListNeinLoopNone(comments_are_optional=False)) # '+43-664-5487520 (Mitterer Alm)'
+        self.add_field(u'Bild', UnicodeNone())
+        self.add_field(u'In Übersichtskarte', GermanBoolNone())
+        self.add_field(u'Forumid', UnsignedNeinNone())