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