#!/usr/bin/python2.7 # -*- coding: iso-8859-15 -*- # $Id$ # $HeadURL$ """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 datetime import re import xml.dom.minidom as minidom from xml.parsers.expat import ExpatError import formencode import formencode.national 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 untouched # 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 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. If a comment is present, the closing bracket has to be the rightmost character. u'' <=> (None, None) u'value' <=> (u'value', None) u'value (comment)' <=> (u'value', u'comment') u'[[link (linkcomment)]]' <=> (u'[[link (linkcomment)]]', None) u'[[link (linkcomment)]] (comment)' <=> (u'[[link (linkcomment)]]', 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: right = value.rfind(')') if right+1 != len(value): if not self.comment_is_optional: raise formencode.Invalid(u'Mandatory comment not present', value, None) v = value c = u'' else: left = value.rfind('(') if left < 0: raise formencode.Invalid(u'Invalid format', value, None) v = value[:left].strip() c = value[left+1:right].strip() 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. "1090.57" 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'%.2f' % (latitude, longitude, height)) else: result.append(u'' % (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(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 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): email = value.replace(self.at, '@') masked = value != email val_email = formencode.validators.Email() return val_email.to_python(email), masked def _from_python(self, value, state): email, masked = value if email is None: return u'' val_email = formencode.validators.Email() email = val_email.from_python(email) 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. 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' 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]]'. 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()))