1
# Copyright (C) 2007-2008 www.stani.be
3
# This program is free software: you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program. If not, see http://www.gnu.org/licenses/
17
Store internally as a string.
18
Provide validation routines.
24
import os, textwrap, types
29
#gui independent (core.lib)
30
from odict import odict as Fields
35
ALIGN_HORIZONTAL = [_t('left'),_t('center'),_t('right')]
36
ALIGN_VERTICAL = [_t('top'),_t('middle'),_t('bottom')]
38
FONT_EXTENSIONS = ['ttf','otf','ttc']
40
IMAGE_EXTENSIONS = ['bmp','dib','gif','jpe','jpeg','jpg','im','msp',
41
'pcx','png','pbm','pgm','ppm','tif','tiff','xbm']
42
IMAGE_READ_EXTENSIONS = IMAGE_EXTENSIONS + ['cur','dcx','fli','flc','fpx',
43
'gbr','gd','ico','imt','mic','mcidas','pcd',
44
'psd','bw','rgb','cmyk','sun','tga','xpm']
45
IMAGE_READ_EXTENSIONS.sort()
46
IMAGE_READ_MIMETYPES = ['image/'+ext for ext in IMAGE_READ_EXTENSIONS]
47
IMAGE_WRITE_EXTENSIONS = IMAGE_EXTENSIONS + ['eps','ps','pdf']
48
IMAGE_WRITE_EXTENSIONS.sort()
49
IMAGE_MODES = [_t('Monochrome (1-bit pixels, black and white)'),
50
_t('Grayscale (8-bit pixels, black and white)'),
51
_t('RGB (3x8-bit pixels, true colour)'),
52
_t('RGBA (4x8-bit pixels, RGB with transparency mask)'),
53
_t('CMYK (4x8-bit pixels, colour separation)'),
54
_t('P (8-bit pixels, mapped using a colour palette)'),
55
_t('YCbCr (3x8-bit pixels, colour video format)'),
56
_t('I (32-bit integer pixels)'),
57
_t('F (32-bit floating point pixels)')]
58
IMAGE_EFFECTS = [_t('blur'), _t('contour'), _t('detail'),
59
_t('edge enhance'), _t('edge enhance more'),
60
_t('emboss'), _t('find edges'), _t('smooth'),
61
_t('smooth more'), _t('sharpen')]
62
IMAGE_FILTERS = [_t('nearest'),_t('bilinear'),_t('bicubic')]
63
IMAGE_RESAMPLE_FILTERS = IMAGE_FILTERS + [_t('antialias')]
64
IMAGE_TRANSPOSE = [_t('Rotate 90'), _t('Rotate 180'), _t('Rotate 270'),
65
_t('Flip Left Right'),_t('Flip Top Bottom')]
67
TEXT_ORIENTATION = [_t('Normal')] + IMAGE_TRANSPOSE
69
IMAGE_MODELS_WRITE_EXTENSIONS = ['<type>']+IMAGE_WRITE_EXTENSIONS
71
IMAGE_READ_EXTENSIONS.sort()
72
IMAGE_WRITE_EXTENSIONS.sort()
77
def ensure_path(path):
78
"""Ensure a path exists, create all not existing paths."""
79
if not os.path.exists(path):
80
parent = os.path.dirname(path)
85
raise OSError, "The path '%s' is not valid."%path
87
def is_www_file(value):
88
return value.startswith('http://') or value.startswith('ftp://')
91
return os.path.isfile(value) or is_www_file(value)
95
#todo: move this as instance attributes
102
def __init__(self,**options):
103
"""For the possible options see the source code."""
105
fields['__enabled__'] = BooleanField(True,visible=False)
106
self.interface(fields)
107
self._fields = fields
108
self._fields.update(options)
110
def interface(self,fields):
113
def __cmp__(self, other):
114
label = _(self.label)
115
other_label = _(other_label)
116
if label < other_label: return -1
117
elif label == other_label: return 0
120
def _get_fields(self):
123
def get_field_labels(self):
124
return self._get_fields().keys()
126
def _get_field(self,label):
127
return self._fields[label]
129
def get_field(self,label,info={}):
130
return self._get_field(label).get(info,label)
132
def get_fields(self,info,convert=False,pixel_fields={}):
134
for label in self.get_field_labels():
135
if label[:2] != '__':
137
#skip hidden fields such as __enabled__
138
if label in pixel_fields:
139
#pixel size -> base, dpi needed
140
param = pixel_fields[label]
141
if type(param) != types.TupleType:
142
param = (param,info[self.dpi])
143
elif self._get_field(label).__class__ == PixelField:
146
value = self.get_field_size(label,info,*param)
148
#retrieve normal value
149
value = self.get_field(label,info)
150
#convert field labels to function parameters
152
label = label.lower().replace(' ','_')
153
result[label] = value
156
def get_field_size(self,label,info,base,dpi):
157
return self._get_field(label).get_size(info,base,dpi,label)
159
def get_field_filesize(self,label,info,base):
160
return self._get_field(label).get_size(info,base,label)
162
def get_field_string(self,label):
163
return self._get_field(label).get_as_string()
165
def is_enabled(self):
166
return self.get_field('__enabled__',None)
168
def _set_field(self,label,field):
169
self._fields[label] = field
171
def set_field(self,label,value):
172
self._get_field(label).set(value)
175
def set_fields(self,**options):
176
for label, value in options.items():
177
self.set_field(label, value)
179
def set_field_as_string(self,label,value_as_string):
180
self._get_field(label).set_as_string(value_as_string)
183
def load(self,fields):
184
"""Load dumped, raw strings."""
186
for label, value in fields.items():
187
if self._fields.has_key(label):
188
self.set_field_as_string(label,value)
190
invalid_labels.append(label)
191
return invalid_labels
194
"""Dump as raw strings"""
195
fields_as_strings = {}
196
for label in self.get_field_labels():
197
fields_as_strings[label] = self.get_field_string(label)
198
return {'label':self.label,'fields':fields_as_strings}
201
def ensure_path(self,path):
202
return ensure_path(path)
204
def is_www_file(self,path):
205
return is_www_file(path)
207
def is_file(self,path):
212
class ValidationError(Exception):
214
def __init__(self, expected, message, details=None):
215
"""ValidationError for invalid input.
217
expected - description of the expected value
218
message - message why validation failed
219
details - eg. which variables are allowed"""
220
self.expected = expected
221
self.message = message
222
self.details = details
228
class PilConstantMixin:
229
def to_python(self,x,label):
230
return x.upper().replace(' ','_')
232
class TestFieldMixin:
233
""" Mixin class, the to_python method should
235
def to_python(self,x,label,test=False):
236
"test parameter to signal test-validate"
241
def get(self,info=None,label='?',value_as_string=None,test=False):
242
"""Use this method to test-validate the user input, for example:
243
field.get(IMAGE_TEST_INFO, value_as_string, label, test=True)"""
244
if value_as_string is None:
245
value_as_string = self.value_as_string
246
return self.to_python(self.interpolate(value_as_string,info,label),
251
"""Base class for fields. This will be subclassed but,
254
Required to overwrite:
255
description - describes the expected value
257
Optional to overwrite
258
to_python - raise here exceptions in case of validation errors (defaults
260
to_string - (defaults to string)
263
validate - will work right out of the box as exceptions are raised by
265
get - gets the current value as a string
266
set - sets the current value as a string
268
You can access the value by self.value_as_string
270
This field interpolates <variables> within a info.
271
<< or >> will be interpolated as < or >
276
def __init__(self,value,visible=True):
277
self.visible = visible
278
if isinstance(value, (str, unicode)):
279
self.set_as_string(value)
283
def interpolate(self,x,info,label):
285
return self.value_as_string
288
return x.replace('%','%%')\
289
.replace('<','%(').replace('>',')s')\
290
.replace('%(%(','<').replace(')s)s','>')%info
291
except KeyError, variable:
292
raise ValidationError(self.description,
293
"%s: %s '%s' %s."%(_(label),_("the variable"),
294
variable.message,_("doesn't exist")),
295
_('Use the Image Inspector to list all the variables.'))
297
def to_python(self,x,label):
300
def to_string(self,x):
303
def get_as_string(self):
304
"""For GUI: Translation, but no interpolation here"""
305
return self.value_as_string
307
def set_as_string(self,x):
308
"""For GUI: Translation, but no interpolation here"""
309
self.value_as_string = x
311
def get(self,info=None,label='?',value_as_string=None,test=False):
312
"""For code: Interpolated, but not translated
313
- value_as_string can be optionally provided to test the expression
315
Ignore test parameter (only for compatiblity with TestField)"""
316
if value_as_string is None:
317
value_as_string = self.value_as_string
318
return self.to_python(self.interpolate(value_as_string,info,label),
322
"""For code: Interpolated, but not translated"""
323
self.value_as_string = self.to_string(x)
325
def eval(self,x,label):
332
raise ValidationError(self.description,
333
'%s: %s "%s" %s.'%(_(label),_('invalid syntax'),x,
336
class IntegerField(Field):
338
description = _('integer')
340
def to_python(self,x,label):
341
error = ValidationError(self.description,
342
'%s: %s "%s" %s.'%(_(label),_('invalid literal'),x,
345
return int(round(self.eval(x,label)))
351
class PositiveIntegerField(IntegerField):
353
description = _('positive integer')
355
def to_python(self,x,label):
356
value = super(PositiveIntegerField, self).to_python(x,label)
358
raise ValidationError(self.description,
359
'%s: %s "%s" %s.'%(_(label),('the integer value'),x,
360
_('is negative, but should be positive')))
363
class PositiveNonZeroIntegerField(PositiveIntegerField):
366
description = _('positive, non-zero integer')
368
def to_python(self,x,label):
369
value = super(PositiveNonZeroIntegerField, self).to_python(x,label)
371
raise ValidationError(self.description,
372
'%s: %s "%s" %s.'%(_(label),_('the integer value'),x,
373
_('is zero, but should be non-zero.')))
376
class DpiField(PositiveNonZeroIntegerField):
377
"""PIL defines the resolution in two dimensions as a tuple (x,y).
378
Phatch ignores this possibility and simplifies by using only one resolution
381
description = _('resolution')
383
class FloatField(Field):
384
description = _('float')
386
def to_python(self,x,label):
388
return float(self.eval(x,label))
389
except ValueError, message:
390
raise ValidationError(self.description,
391
'%s: %s "%s" %s.'%(_(label),_('invalid literal'),x,_('for float')))
393
class PositiveFloatField(FloatField):
395
description = _('positive integer')
397
def to_python(self,x,label):
398
value = super(PositiveFloatField, self).to_python(x,label)
400
raise ValidationError(self.description,
401
'%s: %s "%s" %s.'%(_(label),('the float value'),x,
402
_('is negative, but should be positive')))
405
class PositiveNonZeroFloatField(PositiveIntegerField):
408
description = _('positive, non-zero integer')
410
def to_python(self,x,label):
411
value = super(PositiveNonZeroIntegerField, self).to_python(x,label)
413
raise ValidationError(self.description,
414
'%s: %s "%s" %s.'%(_(label),_('the float value'),x,
415
_('is zero, but should be non-zero.')))
418
class BooleanField(Field):
419
description = _('boolean')
421
def to_string(self,x):
422
return super(BooleanField,self).to_string(x).lower()
424
def to_python(self,x,label):
425
if x.lower() in ['1','true','yes']: return True
426
if x.lower() in ['0','false','no']: return False
427
raise ValidationError(self.description,
428
'%s: %s "%s" %s (%s,%s).'%(_(label),_('invalid literal'), x,
429
_('for boolean'),_('true'),_('false')))
431
class CharField(Field):
432
description = _('string')
435
class ChoiceField(CharField):
436
description = _('choice')
437
def __init__(self,value,choices,**keyw):
438
super(ChoiceField,self).__init__(value,**keyw)
439
self.choices = choices
441
class FileField(CharField):
445
def to_python(self,x,label):
446
value = super(FileField, self).to_python(x,label).strip()
447
if not value and self.allow_empty:
449
ext = os.path.splitext(value)[-1][1:]
450
if self.extensions and not (ext in self.extensions):
452
raise ValidationError(self.description,
453
'%s: %s "%s" %s\n\n%s:\n%s.'%(_(label),
454
_('the file extension'),ext,
456
_('You can only use files with the following extensions'),
457
', '.join(self.extensions)))
459
raise ValidationError(self.description,
462
_('a filename with a valid extension was expected.'),
463
_('You can only use files with the following extensions'),
464
textwrap.fill(', '.join(self.extensions),70)))
467
class ReadFileField(TestFieldMixin,FileField):
468
"""This is a test field to ensure that the file exists.
469
It could also have been called the MustExistFileField."""
471
def to_python(self,x,label,test=False):
472
value = super(ReadFileField, self).to_python(x,label)
473
if not value.strip() and self.allow_empty:
475
if (x==value or not test) and (not is_file(value)):
476
raise ValidationError(self.description,
477
'%s: %s "%s" %s.'%(_(label),_('the filename'),value,
478
_('does not exist.')))
481
class ImageReadFileField(ReadFileField):
482
extensions = IMAGE_READ_EXTENSIONS
484
class FontFileField(ReadFileField):
485
extensions = FONT_EXTENSIONS
488
class FileNameField(CharField):
489
"""Without extension"""
492
class FilePathField(CharField):
495
class ImageTypeField(ChoiceField):
496
def __init__(self,value,**keyw):
497
super(ImageTypeField,self).__init__(value,IMAGE_EXTENSIONS,**keyw)
499
def set_as_string(self,x):
503
super(ImageTypeField,self).set_as_string(x)
505
##class ImageTypeField(ChoiceField):
506
## def set_as_string(self,x):
507
## if x and x[0]=='.':
509
## super(ImageTypeField,self).set_as_string(x)
511
class ImageReadTypeField(ChoiceField):
512
def __init__(self,value,**keyw):
513
super(ImageReadTypeField,self).__init__(\
514
value,IMAGE_READ_EXTENSIONS,**keyw)
516
class ImageWriteTypeField(ChoiceField):
517
def __init__(self,value,**keyw):
518
super(ImageWriteTypeField,self).__init__(\
519
value,IMAGE_MODELS_WRITE_EXTENSIONS,**keyw)
521
class ImageModeField(ChoiceField):
522
def __init__(self,value,**keyw):
523
super(ImageModeField,self).__init__(value,IMAGE_MODES,**keyw)
525
def to_python(self,x,label):
526
return x.split(' ')[0].replace('Grayscale','L').replace('Monochrome','1')
528
class ImageEffectField(PilConstantMixin,ChoiceField):
529
def __init__(self,value,**keyw):
530
super(ImageEffectField,self).__init__(\
531
value,IMAGE_EFFECTS,**keyw)
533
class ImageFilterField(PilConstantMixin,ChoiceField):
534
def __init__(self,value,**keyw):
535
super(ImageFilterField,self).__init__(\
536
value,IMAGE_FILTERS,**keyw)
538
class ImageResampleField(PilConstantMixin,ChoiceField):
539
def __init__(self,value,**keyw):
540
super(ImageResampleField,self).__init__(\
541
value,IMAGE_RESAMPLE_FILTERS,**keyw)
543
class ImageTransposeField(PilConstantMixin,ChoiceField):
544
def __init__(self,value,**keyw):
545
super(ImageTransposeField,self).__init__(\
546
value,IMAGE_TRANSPOSE,**keyw)
548
class TextOrientationField(PilConstantMixin,ChoiceField):
549
def __init__(self,value,**keyw):
550
super(TextOrientationField,self).__init__(\
551
value,TEXT_ORIENTATION,**keyw)
553
def to_python(self,x,label):
554
if x == _t('Normal'):
556
return super(TextOrientationField,self).to_python(x,label)
558
class AlignHorizontalField(ChoiceField):
559
def __init__(self,value,**keyw):
560
super(AlignHorizontalField,self).__init__(\
561
value,ALIGN_HORIZONTAL,**keyw)
563
class AlignVerticalField(ChoiceField):
564
def __init__(self,value,**keyw):
565
super(AlignVerticalField,self).__init__(\
566
value,ALIGN_VERTICAL,**keyw)
568
class RankSizeField(IntegerField,ChoiceField):
569
def __init__(self,value,**keyw):
570
super(RankSizeField,self).__init__(\
571
value,RANK_SIZES,**keyw)
573
class PixelField(IntegerField):
574
"""Can be pixels, cm, inch, %."""
575
def get_size(self,info,base,dpi,label,value_as_string=None):
576
if value_as_string is None:
577
value_as_string = self.value_as_string
578
for unit, value in self._units(base,dpi).items():
579
value_as_string = value_as_string.replace(unit,value)
580
return super(PixelField,self).get(info,label,value_as_string)
582
def _units(self,base,dpi):
584
'cm' : '*%f'%(dpi/2.54),
585
'mm' : '*%f'%(dpi/25.4),
587
'%' : '*%f'%(base/100.0),
591
class FileSizeField(IntegerField):
592
"""Can be pixels, cm, inch, %."""
593
def get_size(self,info,base,label,value_as_string=None):
594
if value_as_string is None:
595
value_as_string = self.value_as_string
596
for unit, value in self._units(base).items():
597
value_as_string = value_as_string.replace(unit,value)
598
return super(FileSizeField,self).get(info,label,value_as_string)
600
def _units(self,base):
603
'%' : '*%f'%(base/100.0),
604
'gb' : '*1073741824',
609
class SliderField(IntegerField):
610
"""A value with boundaries set by a slider."""
611
def __init__(self,value,minValue,maxValue,**keyw):
612
super(SliderField,self).__init__(value,**keyw)
616
class ColourField(CharField):
618
## def to_python(self,x,label):
619
## return eval(x.replace('#','0x'))
622
##class CommaSeparatedIntegerField(CharField):
623
## """Not implemented yet."""
626
##class DateField(Field):
627
## """Not implemented yet."""
630
##class DateTimeField(DateField):
631
## """Not implemented yet."""
634
##class EmailField(CharField):
635
## """Not implemented yet."""
638
##class UrlField(CharField):
639
## """Not implemented yet."""
642
#Give Form all the tools
643
FIELDS = [(name,cls) for name, cls in locals().items()
644
if name[0] != '_' and \
645
((type(cls) == types.TypeType and issubclass(cls,Field)) or\
646
type(cls) in [types.StringType,types.UnicodeType,types.ListType,
650
for name,Field in FIELDS:
651
setattr(Form,name,Field)