Implemented masked_email.
[philipp/winterrodeln/wrpylib.git] / wrpylib / wrvalidators.py
1 #!/usr/bin/python3.4
2 # -*- coding: iso-8859-15 -*-
3 # $Id$
4 # $HeadURL$
5 """
6 A converter is a Python variable (may be a class, class instance or anything else) that has the member
7 functions from_str and to_str. From string takes a string "from the outside", checks it and returns a Python variable
8 representing that value in Python. It reports error by raising ValueError. to_str does the opposite, however, it
9 can assume that the value it has to convert to a string is valid. If it gets an invalid value, the behavior is
10 undefined.
11 """
12 import datetime
13 import email.headerregistry
14 import urllib.parse
15 import re
16 import xml.dom.minidom as minidom
17 from xml.parsers.expat import ExpatError
18 from collections import OrderedDict, namedtuple
19
20 import mwparserfromhell
21 import formencode
22 import formencode.national
23 from wrpylib.mwmarkup import template_to_table
24
25
26 # Meta converter types and functions
27 # ----------------------------------
28
29 FromToConverter = namedtuple('FromToConverter', ['from_str', 'to_str'])
30
31
32 def opt_from_str(value, from_str, none=None):
33     return none if value == '' else from_str(value)
34
35
36 def opt_to_str(value, to_str, none=None):
37     return '' if value == none else to_str(value)
38
39
40 def choice_from_str(value, choices):
41     if value not in choices:
42         raise ValueError('{} is an invalid value')
43     return value
44
45
46 def dictkey_from_str(value, key_str_dict):
47     try:
48         return dict(list(zip(key_str_dict.values(), key_str_dict.keys())))[value]
49     except KeyError:
50         raise ValueError("Invalid value '{}'".format(value))
51
52
53 def dictkey_to_str(value, key_str_dict):
54     try:
55         return key_str_dict[value]
56     except KeyError:
57         raise ValueError("Invalid value '{}'".format(value))
58
59
60 # Basic type converter functions
61 # ------------------------------
62
63
64 def str_from_str(value):
65     return value
66
67
68 def str_to_str(value):
69     return value
70
71
72 def opt_str_from_str(value):
73     return opt_from_str(value, str_from_str)
74
75
76 def opt_str_to_str(value):
77     return opt_to_str(value, str_to_str)
78
79
80 opt_str_converter = FromToConverter(opt_str_from_str, opt_str_to_str)
81
82
83 def req_str_from_str(value):
84     if value == '':
85         raise ValueError('missing required value')
86     return str_from_str(value)
87
88
89 def int_from_str(value, min=None, max=None):
90     value = int(value)
91     if min is not None and value < min:
92         raise ValueError('{} must be >= than {}'.format(value, min))
93     if max is not None and value > max:
94         raise ValueError('{} must be <= than {}'.format(value, max))
95     return value
96
97
98 def int_to_str(value):
99     return str(value)
100
101
102 def opt_int_from_str(value, min=None, max=None):
103     return opt_from_str(value, lambda val: int_from_str(val, min, max))
104
105
106 def opt_int_to_str(value):
107     return opt_to_str(value, int_to_str)
108
109
110 opt_int_converter = FromToConverter(opt_int_from_str, opt_int_to_str)
111
112
113 IntConverter = FromToConverter(int_from_str, int_to_str)
114
115
116 # Complex types
117 # -------------
118
119 def enum_from_str(value, from_str=req_str_from_str, separator=';', min_len=0):
120     """Semicolon separated list of entries with the same "type"."""
121     values = value.split(separator)
122     if len(values) == 1 and values[0] == '':
123         values = []
124     if len(values) < min_len:
125         raise ValueError('at least {} entry/entries have to be in the enumeration'.format(min_len))
126     return list(map(from_str, map(str.strip, values)))
127
128
129 def enum_to_str(value, to_str=opt_str_to_str, separator='; '):
130     return separator.join(map(to_str, value))
131
132
133 # Specific converter functions
134 # ----------------------------
135
136 BOOL_GERMAN = OrderedDict([(False, 'Nein'), (True, 'Ja')])
137
138
139 def bool_german_from_str(value):
140     return dictkey_from_str(value, BOOL_GERMAN)
141
142
143 def bool_german_to_str(value):
144     return dictkey_to_str(value, BOOL_GERMAN)
145
146
147 def opt_bool_german_from_str(value):
148     return opt_from_str(value, bool_german_from_str)
149
150
151 def opt_bool_german_to_str(value):
152     return opt_to_str(value, bool_german_to_str)
153
154
155 opt_bool_german_converter = FromToConverter(opt_bool_german_from_str, opt_bool_german_to_str)
156
157
158 TRISTATE_GERMAN = OrderedDict([(0.0, 'Nein'), (0.5, 'Teilweise'), (1.0, 'Ja')])
159
160
161 def tristate_german_from_str(value):
162     return dictkey_from_str(value, TRISTATE_GERMAN)
163
164
165 def tristate_german_to_str(value):
166     return dictkey_to_str(value, TRISTATE_GERMAN)
167
168
169 def opt_tristate_german_from_str(value):
170     return opt_from_str(value, tristate_german_from_str)
171
172
173 def opt_tristate_german_to_str(value):
174     return opt_to_str(value, tristate_german_to_str)
175
176
177 def meter_from_str(value):
178     return int_from_str(value, min=0)
179
180
181 def meter_to_str(value):
182     return int_to_str(value)
183
184
185 def opt_meter_from_str(value):
186     return opt_from_str(value, meter_from_str)
187
188
189 def opt_meter_to_str(value):
190     return opt_to_str(value, meter_to_str)
191
192
193 opt_meter_converter = FromToConverter(opt_meter_from_str, opt_meter_to_str)
194
195
196 def minutes_from_str(value):
197     return int_from_str(value, min=0)
198
199
200 def minutes_to_str(value):
201     return int_to_str(value)
202
203
204 def opt_minutes_from_str(value):
205     return opt_from_str(value, minutes_from_str)
206
207
208 def opt_minutes_to_str(value):
209     return opt_to_str(value, minutes_to_str)
210
211
212 opt_minutes_converter = FromToConverter(opt_minutes_from_str, opt_minutes_to_str)
213
214
215 LonLat = namedtuple('LonLat', ['lon', 'lat'])
216
217
218 lonlat_none = LonLat(None, None)
219
220
221 def lonlat_from_str(value):
222     """Converts a winterrodeln geo string like '47.076207 N 11.453553 E' (being '<latitude> N <longitude> E'
223     to the LonLat(lon, lat) named  tupel."""
224     r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value)
225     if r is None: raise ValueError("Coordinates '{}' have not a format like '47.076207 N 11.453553 E'".format(value))
226     return LonLat(float(r.groups()[1]), float(r.groups()[0]))
227
228
229 def lonlat_to_str(value):
230     return '{:.6f} N {:.6f} E'.format(value.lat, value.lon)
231
232
233 def opt_lonlat_from_str(value):
234     return opt_from_str(value, lonlat_from_str, lonlat_none)
235
236
237 def opt_lonlat_to_str(value):
238     return opt_to_str(value, lonlat_to_str, lonlat_none)
239
240
241 opt_lonlat_converter = FromToConverter(opt_lonlat_from_str, opt_lonlat_to_str)
242
243
244
245 class MultiGeo(formencode.FancyValidator):
246     "Formats multiple coordinates, even in multiple lines to [(latitude, longitude, elevation), ...] or [(latitude, longitude, None), ...] tuplets."
247     
248     # Valid for input_format
249     FORMAT_GUESS = 0         # guesses the input format; default for input_format
250     FORMAT_NONE = -1          # indicates missing formats
251     
252     # Valid for input_format and output_format
253     FORMAT_GEOCACHING = 1    # e.g. "N 47° 13.692 E 011° 25.535"
254     FORMAT_WINTERRODELN = 2  # e.g. "47.222134 N 11.467211 E"
255     FORMAT_GMAPPLUGIN = 3    # e.g. "47.232922, 11.452239"
256     FORMAT_GPX = 4           # e.g. "<trkpt lat="47.181289" lon="11.408827"><ele>1090.57</ele></trkpt>"
257     
258     input_format = FORMAT_GUESS
259     output_format = FORMAT_WINTERRODELN
260     last_input_format = FORMAT_NONE
261
262     def __init__(self, input_format = FORMAT_GUESS, output_format = FORMAT_WINTERRODELN, **keywords):
263         self.input_format = input_format
264         self.output_format = output_format
265         formencode.FancyValidator.__init__(self, if_empty = (None, None, None), **keywords)
266     
267     def to_python(self, value, state=None):
268         self.assert_string(value, state)
269         input_format = self.input_format
270         if not input_format in [self.FORMAT_GUESS, self.FORMAT_GEOCACHING, self.FORMAT_WINTERRODELN, self.FORMAT_GMAPPLUGIN, self.FORMAT_GPX]:
271             raise formencode.Invalid("input_format %d is not recognized" % input_format, value, state) # Shouldn't it be an other type of runtime error?
272         lines = [line.strip() for line in value.split("\n") if len(line.strip()) > 0]
273         
274         result = []
275         for line in lines:
276             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GEOCACHING:
277                 r = re.match('N ?(\d+)° ?(\d+\.\d+) +E ?(\d+)° ?(\d+\.\d+)', line)
278                 if not r is None:
279                     g = r.groups()
280                     result.append((float(g[0]) + float(g[1])/60, float(g[2]) + float(g[3])/60, None))
281                     last_input_format = self.FORMAT_WINTERRODELN
282                     continue
283                     
284             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_WINTERRODELN:
285                 r = re.match('(\d+\.\d+) N (\d+\.\d+) E', line)
286                 if not r is None:
287                     result.append((float(r.groups()[0]), float(r.groups()[1]), None))
288                     last_input_format = self.FORMAT_WINTERRODELN
289                     continue
290                 
291             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GMAPPLUGIN:
292                 r = re.match('(\d+\.\d+), ?(\d+\.\d+)', line)
293                 if not r is None:
294                     result.append((float(r.groups()[0]), float(r.groups()[1]), None))
295                     last_input_format = self.FORMAT_GMAPPLUGIN
296                     continue
297                 
298             if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GPX:
299                 try:
300                     xml = minidom.parseString(line)
301                     coord = xml.documentElement
302                     lat = float(coord.getAttribute('lat'))
303                     lon = float(coord.getAttribute('lon'))
304                     try: ele = float(coord.childNodes[0].childNodes[0].nodeValue)
305                     except (IndexError, ValueError): ele = None
306                     result.append((lat, lon, ele))
307                     last_input_format = self.FORMAT_GPX
308                     continue
309                 except (ExpatError, IndexError, ValueError): pass
310
311             raise formencode.Invalid("Coordinates '%s' have no known format" % line, value, state)
312             
313         return result
314     
315     def from_python(self, value, state=None):
316         output_format = self.output_format
317         result = []
318         for latitude, longitude, height in value:
319             if output_format == self.FORMAT_GEOCACHING:
320                 degree = latitude
321                 result.append('N %02d° %02.3f E %03d° %02.3f' % (latitude, latitude % 1 * 60, longitude, longitude % 1 * 60))
322                 
323             elif output_format == self.FORMAT_WINTERRODELN:
324                 result.append('%.6f N %.6f E' % (latitude, longitude))
325
326             elif output_format == self.FORMAT_GMAPPLUGIN:
327                 result.append('%.6f, %.6f' % (latitude, longitude))
328                 
329             elif output_format == self.FORMAT_GPX:
330                 if not height is None: result.append('<trkpt lat="%.6f" lon="%.6f"><ele>%.2f</ele></trkpt>' % (latitude, longitude, height))
331                 else: result.append('<trkpt lat="%.6f" lon="%.6f"/>' % (latitude, longitude))
332             
333             else:
334                 raise formencode.Invalid("output_format %d is not recognized" % output_format, value, state) # Shouldn't it be an other type of runtime error?
335             
336         return "\n".join(result)
337
338
339 DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')])
340
341
342 def difficulty_german_from_str(value):
343     return dictkey_from_str(value, DIFFICULTY_GERMAN)
344
345
346 def difficulty_german_to_str(value):
347     return dictkey_to_str(value, DIFFICULTY_GERMAN)
348
349
350 def opt_difficulty_german_from_str(value):
351     return opt_from_str(value, difficulty_german_from_str)
352
353
354 def opt_difficulty_german_to_str(value):
355     return opt_to_str(value, difficulty_german_to_str)
356
357
358 opt_difficulty_german_converter = FromToConverter(opt_difficulty_german_from_str, opt_difficulty_german_to_str)
359
360
361 AVALANCHES_GERMAN = OrderedDict([(1, 'kaum'), (2, 'selten'), (3, 'gelegentlich'), (4, 'häufig')])
362
363
364 def avalanches_german_from_str(value):
365     return dictkey_from_str(value, AVALANCHES_GERMAN)
366
367
368 def avalanches_german_to_str(value):
369     return dictkey_to_str(value, AVALANCHES_GERMAN)
370
371
372 def opt_avalanches_german_from_str(value):
373     return opt_from_str(value, avalanches_german_from_str)
374
375
376 def opt_avalanches_german_to_str(value):
377     return opt_to_str(value, avalanches_german_to_str)
378
379
380 opt_avalanches_german_converter = FromToConverter(opt_avalanches_german_from_str, opt_avalanches_german_to_str)
381
382
383 PUBLIC_TRANSPORT_GERMAN = OrderedDict([(1, 'Sehr gut'), (2, 'Gut'), (3, 'Mittelmäßig'), (4, 'Schlecht'), (5, 'Nein'), (6, 'Ja')])
384
385
386 def public_transport_german_from_str(value):
387     return dictkey_from_str(value, PUBLIC_TRANSPORT_GERMAN)
388
389
390 def public_transport_german_to_str(value):
391     return dictkey_to_str(value, PUBLIC_TRANSPORT_GERMAN)
392
393
394 def opt_public_transport_german_from_str(value):
395     return opt_from_str(value, public_transport_german_from_str)
396
397
398 def opt_public_transport_german_to_str(value):
399     return opt_to_str(value, public_transport_german_to_str)
400
401
402 opt_public_transport_german_converter = FromToConverter(opt_public_transport_german_from_str, opt_public_transport_german_to_str)
403
404
405 def value_comment_from_str(value, value_from_str=str_from_str, comment_from_str=str_from_str, comment_optional=False):
406     """Makes it possible to have a mandatory comment in parenthesis at the end of the string."""
407     open_brackets = 0
408     comment = ''
409     comment_end_pos = None
410     for i, char in enumerate(value[::-1]):
411         if char == ')':
412             open_brackets += 1
413             if open_brackets == 1:
414                 comment_end_pos = i
415                 if len(value[-1-comment_end_pos:].rstrip()) > 1:
416                     raise ValueError('invalid characters after comment')
417         elif char == '(':
418             open_brackets -= 1
419             if open_brackets == 0:
420                 comment = value[-i:-1-comment_end_pos]
421                 value = value[:-i-1].rstrip()
422                 break
423     else:
424         if open_brackets > 0:
425             raise ValueError('bracket mismatch')
426         if not comment_optional:
427             raise ValueError('mandatory comment not found')
428     return value_from_str(value), comment_from_str(comment)
429
430
431 def value_comment_to_str(value, value_to_str=str_to_str, comment_to_str=str_to_str, comment_optional=False):
432     left = value_to_str(value[0])
433     comment = comment_to_str(value[1])
434     if len(comment) > 0 or not comment_optional:
435         comment = '({})'.format(comment)
436     if len(left) == 0:
437         return comment
438     if len(comment) == 0:
439         return left
440     return '{} {}'.format(left, comment)
441
442
443 def opt_tristate_german_comment_from_str(value):
444     """Ja, Nein or Vielleicht, optionally with comment in parenthesis."""
445     return value_comment_from_str(value, opt_tristate_german_from_str, opt_str_from_str, True)
446
447
448 def opt_tristate_german_comment_to_str(value):
449     return value_comment_to_str(value, opt_tristate_german_to_str, opt_str_to_str, True)
450
451
452 opt_tristate_german_comment_converter = FromToConverter(opt_tristate_german_comment_from_str, opt_tristate_german_comment_to_str)
453
454
455 def no_german_from_str(value, from_str=req_str_from_str, use_tuple=True, no_value=None):
456     if value == 'Nein':
457         return (False, no_value) if use_tuple else no_value
458     return (True, from_str(value)) if use_tuple else from_str(value)
459
460
461 def no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None):
462     if use_tuple:
463         if not value[0]:
464             return 'Nein'
465         return to_str(value[1])
466     else:
467         if value == no_value:
468             return 'Nein'
469         return to_str(value)
470
471
472 def opt_no_german_from_str(value, from_str=str_from_str, use_tuple=True, no_value=None, none=(None, None)):
473     return opt_from_str(value, lambda v: no_german_from_str(v, from_str, use_tuple, no_value), none)
474
475
476 def opt_no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None, none=(None, None)):
477     return opt_to_str(value, lambda v: no_german_to_str(v, to_str, use_tuple, no_value), none)
478
479
480 def night_light_from_str(value):
481     """'Beleuchtungsanlage' Tristate with optional comment:
482     ''                  <=> (None, None)
483     'Ja'                <=> (1.0,  None)
484     'Teilweise'         <=> (0.5,  None)
485     'Nein'              <=> (0.0,  None)
486     'Ja (aber schmal)'  <=> (1.0,  'aber schmal')
487     'Teilweise (oben)'  <=> (0.5,  'oben')
488     'Nein (aber breit)' <=> (0.0,  'aber breit')
489     """
490     return
491
492
493 def nightlightdays_from_str(value):
494     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)
495
496
497 def nightlightdays_to_str(value):
498     return value_comment_to_str(value, lambda val: opt_to_str(val, int_to_str), opt_str_to_str, comment_optional=True)
499
500
501 nightlightdays_converter = FromToConverter(nightlightdays_from_str, nightlightdays_to_str)
502
503
504 CACHET_REGEXP = [r'(Tiroler Naturrodelbahn-Gütesiegel) ([12]\d{3}) (leicht|mittel|schwer)$']
505
506
507 def single_cachet_german_from_str(value):
508     for pattern in CACHET_REGEXP:
509         match = re.match(pattern, value)
510         if match:
511             return match.groups()
512     raise ValueError("'{}' is no valid cachet".format(value))
513
514
515 def single_cachet_german_to_str(value):
516     return ' '.join(value)
517
518
519 def cachet_german_from_str(value):
520     """Converts a "Gütesiegel":
521     '' => None
522     'Nein' => []
523     'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' => [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]"""
524     return opt_no_german_from_str(value, lambda val: enum_from_str(val, single_cachet_german_from_str), False, [], None)
525
526     
527 def cachet_german_to_str(value):
528     return opt_no_german_to_str(value, lambda val: enum_to_str(val, single_cachet_german_to_str), False, [], None)
529
530
531 cachet_german_converter = FromToConverter(cachet_german_from_str, cachet_german_to_str)
532
533
534 def url_from_str(value):
535     result = urllib.parse.urlparse(value)
536     if result.scheme not in ['http', 'https']:
537         raise ValueError('scheme has to be http or https')
538     if not result.netloc:
539         raise ValueError('url does not contain netloc')
540     return value
541
542
543 def url_to_str(value):
544     return value
545
546
547 def webauskunft_from_str(value):
548     return opt_no_german_from_str(value, url_from_str)
549
550
551 def webauskunft_to_str(value):
552     return opt_no_german_to_str(value, url_to_str)
553
554
555 webauskunft_converter = FromToConverter(webauskunft_from_str, webauskunft_to_str)
556
557
558 class Url(formencode.FancyValidator):
559     """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed."""
560     # formencode 1.2.5 to formencode 1.3.0a1 sometimes raise ValueError instead of Invalid exceptions
561     # https://github.com/formencode/formencode/pull/61
562     urlv = formencode.validators.URL()    
563
564     def to_python(self, value, state=None):
565         self.assert_string(value, state)
566         v = value
567         v = v.replace('ä', 'a')
568         v = v.replace('ö', 'o')
569         v = v.replace('ü', 'u')
570         v = v.replace('ß', 'ss')
571         v = self.urlv.to_python(v, state)
572         return value
573     
574     def from_python(self, value, state=None):
575         return value
576
577
578 def phone_number_from_str(value):
579     match = re.match(r'\+\d+(-\d+)*$', value)
580     if match is None:
581         raise ValueError('invalid format of phone number - use something like +43-699-1234567')
582     return value
583
584
585 def phone_number_to_str(value):
586     return value
587
588
589 def telefonauskunft_from_str(value):
590     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)
591
592
593 def telefonauskunft_to_str(value):
594     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)
595
596
597 telefonauskunft_converter = FromToConverter(telefonauskunft_from_str, telefonauskunft_to_str)
598
599
600 def email_from_str(value):
601     """Takes an email address like 'office@example.com', checks it for correctness and returns it again as string."""
602     try:
603         email.headerregistry.Address(addr_spec=value)
604     except email.errors.HeaderParseError as e:
605         raise ValueError('Invalid email address: {}'.format(value), e)
606     return value
607
608
609 def email_to_str(value):
610     return str(value)
611
612
613 def masked_email_from_str(value, mask='(at)', masked_only=False):
614     """Converts an email address that is possibly masked. Returns a tuple. The first parameter is the un-masked
615     email address as string, the second is a boolean telling whether the address was masked."""
616     unmasked = value.replace(mask, '@')
617     was_masked = unmasked != value
618     if masked_only and not was_masked:
619         raise ValueError('E-Mail address not masked')
620     return email_from_str(unmasked), was_masked
621
622
623 def masked_email_to_str(value, mask='(at)'):
624     """Value is a tuple. The first entry is the email address, the second one is a boolean telling whether the
625     email address should be masked."""
626     email, do_masking = value
627     email = email_to_str(email)
628     if do_masking:
629         email = email.replace('@', mask)
630     return email
631
632
633
634
635 class MaskedEmail(formencode.FancyValidator):
636     """A masked email address as defined here is an email address that has the `@` character replacted by the text `(at)`.
637     So instead of `abd.def@example.com` it would be `abc.def(at)example.com`.
638     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
639     as a bool indicating whether the email address was masked.
640     u''                       <=> (None, None)
641     u'abc.def@example.com'    <=> (u'abc.def@example.com', False)
642     u'abc.def(at)example.com' <=> (u'abc.def@example.com', True)
643     
644     """
645     def __init__(self, *args, **kw):
646         if 'strip' not in kw: kw['strip'] = True
647         if 'not_empty' not in kw: kw['not_empty'] = False
648         if 'if_empty' not in kw: kw['if_empty'] = (None, None)
649         self.at = '(at)'
650         formencode.FancyValidator.__init__(self, *args, **kw)
651
652     def _to_python(self, value, state=None):
653         email = value.replace(self.at, '@')
654         masked = value != email
655         val_email = formencode.validators.Email()
656         return val_email.to_python(email, state), masked
657
658     def _from_python(self, value, state=None):
659         email, masked = value
660         if email is None: return ''
661         val_email = formencode.validators.Email()
662         email = val_email.from_python(email, state)
663         if masked: email = email.replace('@', self.at)
664         return email
665
666
667 '''
668 class EmailCommentListNeinLoopNone(NoneValidator):
669     """Converts a semicolon-separated list of email addresses with optional comments to itself.
670     The special value of u'Nein' indicates that there are no email addresses.
671     The empty string translates to None:
672     u''                                                   <=> None
673     u'Nein'                                               <=> u'Nein'
674     u'first@example.com'                                  <=> u'first@example.com'
675     u'first@example.com (Nur Winter); second@example.com' <=> u'first@example.com (Nur Winter); second@example.com'
676
677     If the parameter allow_masked_email is true, the following gives no error:
678     u'abc.def(at)example.com (comment)'                   <=> u'abc.def(at)example.com (comment)'
679     """
680     def __init__(self, allow_masked_email=False):
681         NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(MaskedEmail() if allow_masked_email else formencode.validators.Email()))))
682 '''
683
684 class WikiPage(formencode.FancyValidator):
685     """Validates wiki page name like u'[[Birgitzer Alm]]'.
686     The page is not checked for existance.
687     An empty string is an error.
688     u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
689     """
690     def to_python(self, value, state=None):
691         self.assert_string(value, state)
692         if not value.startswith('[[') or not value.endswith(']]'): 
693             raise formencode.Invalid('No valid wiki page name', value, state)
694         return value
695     
696     def from_python(self, value, state=None):
697         return value
698
699
700 '''
701 class WikiPageList(SemicolonList):
702     """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]'.
703     u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> [u'[[Birgitzer Alm]]', u'[[Kemater Alm]]']
704     u'[[Birgitzer Alm]]'                  <=> [u'[[Birgitzer Alm]]']
705     u''                                   <=> []
706     """
707     def __init__(self):
708         SemicolonList.__init__(self, WikiPage())
709 '''
710
711
712 '''
713 class WikiPageListLoopNone(NoneValidator):
714     """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]' as string.
715     u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> u'[[Birgitzer Alm]]; [[Kemater Alm]]'
716     u'[[Birgitzer Alm]]'                  <=> u'[[Birgitzer Alm]]'
717     u''                                   <=> None
718     """
719     def __init__(self):
720         NoneValidator.__init__(self, Loop(WikiPageList()))
721 '''
722
723
724 LIFT_GERMAN = ['Sessellift', 'Gondel', 'Linienbus', 'Taxi', 'Sonstige']
725
726
727 def lift_german_from_str(value):
728     """Checks a lift_details property. It is a value comment property with the following
729     values allowed:
730     'Sessellift'
731     'Gondel'
732     'Linienbus'
733     'Taxi'
734     'Sonstige'
735     Alternatively, the value u'Nein' is allowed.
736     An empty string maps to (None, None).
737
738     Examples:
739     ''                                       <=> None
740     'Nein'                                   <=> []
741     'Sessellift                              <=> [('Sessellift', None)]
742     'Gondel (nur bis zur Hälfte)'            <=> [('Gondel', 'nur bis zur Hälfte')]
743     'Sessellift; Taxi'                       <=> [('Sessellift', None), ('Taxi', None)]
744     'Sessellift (Wochenende); Taxi (6 Euro)' <=> [('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')]
745     """
746     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)
747
748
749 def lift_german_to_str(value):
750     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)
751
752
753 lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str)
754
755
756 def sledrental_from_str(value):
757     """The value can be an empty string, 'Nein' or a semicolon-separated list of strings with optional comments.
758     ''                                       => None
759     'Nein'                                   => []
760     'Talstation (nur mit Ticket); Schneealm' => [('Talstation', 'nur mit Ticket'), ('Schneealm', None)]"""
761     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)
762
763
764 def sledrental_to_str(value):
765     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)
766
767
768 sledrental_converter = FromToConverter(sledrental_from_str, sledrental_to_str)
769
770
771 class ValueErrorList(ValueError):
772     pass
773
774
775 def box_from_template(template, name, converter_dict):
776     if template.name.strip() != name:
777         raise ValueError('Box name has to be "{}"'.format(name))
778     result = OrderedDict()
779     exceptions_dict = OrderedDict()
780     # check values
781     for key, converter in converter_dict.items():
782         try:
783             if not template.has(key):
784                 raise ValueError('Missing parameter "{}"'.format(key))
785             result[key] = converter.from_str(str(template.get(key).value.strip()))
786         except ValueError as e:
787             exceptions_dict[key] = e
788     # check if keys are superfluous
789     superfluous_keys = {str(p.name.strip()) for p in template.params} - set(converter_dict.keys())
790     for key in superfluous_keys:
791         exceptions_dict[key] = ValueError('Superfluous parameter: "{}"'.format(key))
792     if len(exceptions_dict) > 0:
793         raise ValueErrorList('{} error(s) occurred when parsing template parameters.'.format(len(exceptions_dict)), exceptions_dict)
794     return result
795
796
797 def box_to_template(value, name, converter_dict):
798     template = mwparserfromhell.nodes.template.Template(name)
799     for key, converter in converter_dict.items():
800         template.add(key, converter.to_str(value[key]))
801     return template
802
803
804 def template_from_str(value, name):
805     wikicode = mwparserfromhell.parse(value)
806     template_list = wikicode.filter_templates(name)
807     if len(name) == 0:
808         raise ValueError('No "{}" template was found'.format(name))
809     if len(template_list) > 1:
810         raise ValueError('{} "{}" templates were found'.format(len(template_list), name))
811     return template_list[0]
812
813
814 def box_from_str(value, name, converter_dict):
815     template = template_from_str(value, name)
816     return box_from_template(template, name, converter_dict)
817
818
819 def box_to_str(value, name, converter_dict):
820     return str(box_to_template(value, name, converter_dict))
821
822
823 RODELBAHNBOX_TEMPLATE_NAME = 'Rodelbahnbox'
824
825
826 RODELBAHNBOX_DICT = OrderedDict([
827     ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
828     ('Position oben', opt_lonlat_converter), # '47.583333 N 15.75 E'
829     ('Höhe oben', opt_meter_converter), # '2000'
830     ('Position unten', opt_lonlat_converter), # '47.583333 N 15.75 E'
831     ('Höhe unten', opt_meter_converter), # '1200'
832     ('Länge', opt_meter_converter), # 3500
833     ('Schwierigkeit', opt_difficulty_german_converter), # 'mittel'
834     ('Lawinen', opt_avalanches_german_converter), # 'kaum'
835     ('Betreiber', opt_str_converter), # 'Max Mustermann'
836     ('Öffentliche Anreise', opt_public_transport_german_converter), # 'Mittelmäßig'
837     ('Aufstieg möglich', opt_bool_german_converter), # 'Ja'
838     ('Aufstieg getrennt', opt_tristate_german_comment_converter), # 'Ja'
839     ('Gehzeit', opt_minutes_converter), # 90
840     ('Aufstiegshilfe', lift_german_converter), # 'Gondel (unterer Teil)'
841     ('Beleuchtungsanlage', opt_tristate_german_comment_converter),
842     ('Beleuchtungstage', nightlightdays_converter), # '3 (Montag, Mittwoch, Freitag)'
843     ('Rodelverleih', sledrental_converter), # 'Talstation Serlesbahnan'
844     ('Gütesiegel', cachet_german_converter), # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
845     ('Webauskunft', webauskunft_converter), # 'http://www.nösslachhütte.at/page9.php'
846     ('Telefonauskunft', telefonauskunft_converter), # '+43-664-5487520 (Mitterer Alm)'
847     ('Bild', opt_str_converter),
848     ('In Übersichtskarte', opt_bool_german_converter),
849     ('Forumid', opt_int_converter)
850 ])
851
852
853 def rodelbahnbox_from_template(template):
854     return box_from_template(template, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
855
856
857 def rodelbahnbox_to_template(value):
858     return box_to_template(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
859
860
861 def rodelbahnbox_from_str(value):
862     return box_from_str(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
863
864
865 def rodelbahnbox_to_str(value):
866     template = rodelbahnbox_to_template(value)
867     template_to_table(template, 20)
868     return str(template)
869
870
871 GASTHAUSBOX_TEMPLATE_NAME = 'Gasthausbox'
872
873
874 '''
875 GASTHAUSBOX_DICT = OrderedDict([
876     ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
877     ('Höhe', opt_meter_converter),
878     ('Betreiber', opt_str_converter),
879     ('Sitzplätze', opt_int_converter),
880     ('Übernachtung', BoolUnicodeTupleValidator()),
881     ('Rauchfrei', opt_tristate_german_validator),
882     ('Rodelverleih', BoolUnicodeTupleValidator()),
883     ('Handyempfang', ValueCommentListNeinLoopNone()),
884     ('Homepage', webauskunft_converter),
885     ('E-Mail', EmailCommentListNeinLoopNone(allow_masked_email=True)),
886     ('Telefon', PhoneCommentListNeinLoopNone(comments_are_optional=True)),
887     ('Bild', opt_str_converter),
888     ('Rodelbahnen', WikiPageListLoopNone())])
889 '''
890
891
892 def sledrun_page_title_to_pretty_url(page_title):
893     """Converts a page_title from the page_title column of wrsledruncache to name_url.
894     name_url is not used by MediaWiki but by new applications like wrweb."""
895     return page_title.lower().replace(' ', '-').replace('_', '-').replace('(', '').replace(')', '')