| 122 | | from django.utils.html import escape |
|---|
| 123 | | import datetime |
|---|
| 124 | | import re |
|---|
| 125 | | import time |
|---|
| 126 | | |
|---|
| 127 | | # Default encoding for input byte strings. |
|---|
| 128 | | DEFAULT_ENCODING = 'utf-8' # TODO: First look at django.conf.settings, then fall back to this. |
|---|
| 129 | | |
|---|
| 130 | | def smart_unicode(s): |
|---|
| 131 | | if not isinstance(s, unicode): |
|---|
| 132 | | s = unicode(s, DEFAULT_ENCODING) |
|---|
| 133 | | return s |
|---|
| 134 | | |
|---|
| 135 | | ################### |
|---|
| 136 | | # VALIDATOR STUFF # |
|---|
| 137 | | ################### |
|---|
| 138 | | |
|---|
| 139 | | class ErrorDict(dict): |
|---|
| 140 | | """ |
|---|
| 141 | | A collection of errors that knows how to display itself in various formats. |
|---|
| 142 | | |
|---|
| 143 | | The dictionary keys are the field names, and the values are the errors. |
|---|
| 144 | | """ |
|---|
| 145 | | def __str__(self): |
|---|
| 146 | | return self.as_ul() |
|---|
| 147 | | |
|---|
| 148 | | def as_ul(self): |
|---|
| 149 | | if not self: return u'' |
|---|
| 150 | | return u'<ul class="errorlist">%s</ul>' % ''.join([u'<li>%s%s</li>' % (k, v) for k, v in self.items()]) |
|---|
| 151 | | |
|---|
| 152 | | def as_text(self): |
|---|
| 153 | | return u'\n'.join([u'* %s\n%s' % (k, u'\n'.join([u' * %s' % i for i in v])) for k, v in self.items()]) |
|---|
| 154 | | |
|---|
| 155 | | class ErrorList(list): |
|---|
| 156 | | """ |
|---|
| 157 | | A collection of errors that knows how to display itself in various formats. |
|---|
| 158 | | """ |
|---|
| 159 | | def __str__(self): |
|---|
| 160 | | return self.as_ul() |
|---|
| 161 | | |
|---|
| 162 | | def as_ul(self): |
|---|
| 163 | | if not self: return u'' |
|---|
| 164 | | return u'<ul class="errorlist">%s</ul>' % ''.join([u'<li>%s</li>' % e for e in self]) |
|---|
| 165 | | |
|---|
| 166 | | def as_text(self): |
|---|
| 167 | | if not self: return u'' |
|---|
| 168 | | return u'\n'.join([u'* %s' % e for e in self]) |
|---|
| 169 | | |
|---|
| 170 | | class ValidationError(Exception): |
|---|
| 171 | | def __init__(self, message): |
|---|
| 172 | | "ValidationError can be passed a string or a list." |
|---|
| 173 | | if isinstance(message, list): |
|---|
| 174 | | self.messages = ErrorList([smart_unicode(msg) for msg in message]) |
|---|
| 175 | | else: |
|---|
| 176 | | assert isinstance(message, basestring), ("%s should be a basestring" % repr(message)) |
|---|
| 177 | | message = smart_unicode(message) |
|---|
| 178 | | self.messages = ErrorList([message]) |
|---|
| 179 | | |
|---|
| 180 | | def __str__(self): |
|---|
| 181 | | # This is needed because, without a __str__(), printing an exception |
|---|
| 182 | | # instance would result in this: |
|---|
| 183 | | # AttributeError: ValidationError instance has no attribute 'args' |
|---|
| 184 | | # See http://www.python.org/doc/current/tut/node10.html#handling |
|---|
| 185 | | return repr(self.messages) |
|---|
| 186 | | |
|---|
| 187 | | ################ |
|---|
| 188 | | # HTML WIDGETS # |
|---|
| 189 | | ################ |
|---|
| 190 | | |
|---|
| 191 | | # Converts a dictionary to a single string with key="value", XML-style. |
|---|
| 192 | | # Assumes keys do not need to be XML-escaped. |
|---|
| 193 | | flatatt = lambda attrs: ' '.join(['%s="%s"' % (k, escape(v)) for k, v in attrs.items()]) |
|---|
| 194 | | |
|---|
| 195 | | class Widget(object): |
|---|
| 196 | | def __init__(self, attrs=None): |
|---|
| 197 | | self.attrs = attrs or {} |
|---|
| 198 | | |
|---|
| 199 | | def render(self, name, value): |
|---|
| 200 | | raise NotImplementedError |
|---|
| 201 | | |
|---|
| 202 | | class TextInput(Widget): |
|---|
| 203 | | """ |
|---|
| 204 | | >>> w = TextInput() |
|---|
| 205 | | >>> w.render('email', '') |
|---|
| 206 | | u'<input type="text" name="email" />' |
|---|
| 207 | | >>> w.render('email', None) |
|---|
| 208 | | u'<input type="text" name="email" />' |
|---|
| 209 | | >>> w.render('email', 'test@example.com') |
|---|
| 210 | | u'<input type="text" name="email" value="test@example.com" />' |
|---|
| 211 | | >>> w.render('email', 'some "quoted" & ampersanded value') |
|---|
| 212 | | u'<input type="text" name="email" value="some "quoted" & ampersanded value" />' |
|---|
| 213 | | >>> w.render('email', 'test@example.com', attrs={'class': 'fun'}) |
|---|
| 214 | | u'<input type="text" name="email" value="test@example.com" class="fun" />' |
|---|
| 215 | | |
|---|
| 216 | | You can also pass 'attrs' to the constructor: |
|---|
| 217 | | >>> w = TextInput(attrs={'class': 'fun'}) |
|---|
| 218 | | >>> w.render('email', '') |
|---|
| 219 | | u'<input type="text" class="fun" name="email" />' |
|---|
| 220 | | >>> w.render('email', 'foo@example.com') |
|---|
| 221 | | u'<input type="text" class="fun" value="foo@example.com" name="email" />' |
|---|
| 222 | | |
|---|
| 223 | | 'attrs' passed to render() get precedence over those passed to the constructor: |
|---|
| 224 | | >>> w = TextInput(attrs={'class': 'pretty'}) |
|---|
| 225 | | >>> w.render('email', '', attrs={'class': 'special'}) |
|---|
| 226 | | u'<input type="text" class="special" name="email" />' |
|---|
| 227 | | """ |
|---|
| 228 | | def render(self, name, value, attrs=None): |
|---|
| 229 | | if value in EMPTY_VALUES: value = '' |
|---|
| 230 | | final_attrs = dict(self.attrs, type='text', name=name) |
|---|
| 231 | | if attrs: |
|---|
| 232 | | final_attrs.update(attrs) |
|---|
| 233 | | if value != '': final_attrs['value'] = value # Only add the 'value' attribute if a value is non-empty. |
|---|
| 234 | | return u'<input %s />' % flatatt(final_attrs) |
|---|
| 235 | | |
|---|
| 236 | | class Textarea(Widget): |
|---|
| 237 | | """ |
|---|
| 238 | | >>> w = Textarea() |
|---|
| 239 | | >>> w.render('msg', '') |
|---|
| 240 | | u'<textarea name="msg"></textarea>' |
|---|
| 241 | | >>> w.render('msg', None) |
|---|
| 242 | | u'<textarea name="msg"></textarea>' |
|---|
| 243 | | >>> w.render('msg', 'value') |
|---|
| 244 | | u'<textarea name="msg">value</textarea>' |
|---|
| 245 | | >>> w.render('msg', 'some "quoted" & ampersanded value') |
|---|
| 246 | | u'<textarea name="msg">some "quoted" & ampersanded value</textarea>' |
|---|
| 247 | | >>> w.render('msg', 'value', attrs={'class': 'pretty'}) |
|---|
| 248 | | u'<textarea name="msg" class="pretty">value</textarea>' |
|---|
| 249 | | |
|---|
| 250 | | You can also pass 'attrs' to the constructor: |
|---|
| 251 | | >>> w = Textarea(attrs={'class': 'pretty'}) |
|---|
| 252 | | >>> w.render('msg', '') |
|---|
| 253 | | u'<textarea class="pretty" name="msg"></textarea>' |
|---|
| 254 | | >>> w.render('msg', 'example') |
|---|
| 255 | | u'<textarea class="pretty" name="msg">example</textarea>' |
|---|
| 256 | | |
|---|
| 257 | | 'attrs' passed to render() get precedence over those passed to the constructor: |
|---|
| 258 | | >>> w = Textarea(attrs={'class': 'pretty'}) |
|---|
| 259 | | >>> w.render('msg', '', attrs={'class': 'special'}) |
|---|
| 260 | | u'<textarea class="special" name="msg"></textarea>' |
|---|
| 261 | | """ |
|---|
| 262 | | def render(self, name, value, attrs=None): |
|---|
| 263 | | if value in EMPTY_VALUES: value = '' |
|---|
| 264 | | final_attrs = dict(self.attrs, name=name) |
|---|
| 265 | | if attrs: |
|---|
| 266 | | final_attrs.update(attrs) |
|---|
| 267 | | return u'<textarea %s>%s</textarea>' % (flatatt(final_attrs), escape(value)) |
|---|
| 268 | | |
|---|
| 269 | | class CheckboxInput(Widget): |
|---|
| 270 | | """ |
|---|
| 271 | | >>> w = CheckboxInput() |
|---|
| 272 | | >>> w.render('is_cool', '') |
|---|
| 273 | | u'<input type="checkbox" name="is_cool" />' |
|---|
| 274 | | >>> w.render('is_cool', False) |
|---|
| 275 | | u'<input type="checkbox" name="is_cool" />' |
|---|
| 276 | | >>> w.render('is_cool', True) |
|---|
| 277 | | u'<input checked="checked" type="checkbox" name="is_cool" />' |
|---|
| 278 | | >>> w.render('is_cool', False, attrs={'class': 'pretty'}) |
|---|
| 279 | | u'<input type="checkbox" name="is_cool" class="pretty" />' |
|---|
| 280 | | |
|---|
| 281 | | You can also pass 'attrs' to the constructor: |
|---|
| 282 | | >>> w = CheckboxInput(attrs={'class': 'pretty'}) |
|---|
| 283 | | >>> w.render('is_cool', '') |
|---|
| 284 | | u'<input type="checkbox" class="pretty" name="is_cool" />' |
|---|
| 285 | | |
|---|
| 286 | | 'attrs' passed to render() get precedence over those passed to the constructor: |
|---|
| 287 | | >>> w = CheckboxInput(attrs={'class': 'pretty'}) |
|---|
| 288 | | >>> w.render('is_cool', '', attrs={'class': 'special'}) |
|---|
| 289 | | u'<input type="checkbox" class="special" name="is_cool" />' |
|---|
| 290 | | """ |
|---|
| 291 | | def render(self, name, value, attrs=None): |
|---|
| 292 | | final_attrs = dict(self.attrs, type='checkbox', name=name) |
|---|
| 293 | | if attrs: |
|---|
| 294 | | final_attrs.update(attrs) |
|---|
| 295 | | if value: final_attrs['checked'] = 'checked' |
|---|
| 296 | | return u'<input %s />' % flatatt(final_attrs) |
|---|
| 297 | | |
|---|
| 298 | | ########## |
|---|
| 299 | | # FIELDS # |
|---|
| 300 | | ########## |
|---|
| 301 | | |
|---|
| 302 | | # These values, if given to to_python(), will trigger the self.required check. |
|---|
| 303 | | EMPTY_VALUES = (None, '') |
|---|
| 304 | | |
|---|
| 305 | | class Field(object): |
|---|
| 306 | | widget = TextInput # Default widget to use when rendering this type of Field. |
|---|
| 307 | | |
|---|
| 308 | | def __init__(self, required=True, widget=None): |
|---|
| 309 | | self.required = required |
|---|
| 310 | | widget = widget or self.widget |
|---|
| 311 | | if isinstance(widget, type): |
|---|
| 312 | | widget = widget() |
|---|
| 313 | | self.widget = widget |
|---|
| 314 | | |
|---|
| 315 | | def to_python(self, value): |
|---|
| 316 | | """ |
|---|
| 317 | | Validates the given value and returns its "normalized" value as an |
|---|
| 318 | | appropriate Python object. |
|---|
| 319 | | |
|---|
| 320 | | Raises ValidationError for any errors. |
|---|
| 321 | | """ |
|---|
| 322 | | if self.required and value in EMPTY_VALUES: |
|---|
| 323 | | raise ValidationError(u'This field is required.') |
|---|
| 324 | | return value |
|---|
| 325 | | |
|---|
| 326 | | class CharField(Field): |
|---|
| 327 | | """ |
|---|
| 328 | | >>> f = CharField(required=False) |
|---|
| 329 | | >>> f.to_python(1) |
|---|
| 330 | | u'1' |
|---|
| 331 | | >>> f.to_python('hello') |
|---|
| 332 | | u'hello' |
|---|
| 333 | | >>> f.to_python(None) |
|---|
| 334 | | u'' |
|---|
| 335 | | >>> f.to_python([1, 2, 3]) |
|---|
| 336 | | u'[1, 2, 3]' |
|---|
| 337 | | |
|---|
| 338 | | CharField accepts an optional max_length parameter: |
|---|
| 339 | | >>> f = CharField(max_length=10, required=False) |
|---|
| 340 | | >>> f.to_python('') |
|---|
| 341 | | u'' |
|---|
| 342 | | >>> f.to_python('12345') |
|---|
| 343 | | u'12345' |
|---|
| 344 | | >>> f.to_python('1234567890') |
|---|
| 345 | | u'1234567890' |
|---|
| 346 | | >>> f.to_python('1234567890a') |
|---|
| 347 | | Traceback (most recent call last): |
|---|
| 348 | | ... |
|---|
| 349 | | ValidationError: [u'Ensure this value has at most 10 characters.'] |
|---|
| 350 | | |
|---|
| 351 | | CharField accepts an optional min_length parameter: |
|---|
| 352 | | >>> f = CharField(min_length=10, required=False) |
|---|
| 353 | | >>> f.to_python('') |
|---|
| 354 | | Traceback (most recent call last): |
|---|
| 355 | | ... |
|---|
| 356 | | ValidationError: [u'Ensure this value has at least 10 characters.'] |
|---|
| 357 | | >>> f.to_python('12345') |
|---|
| 358 | | Traceback (most recent call last): |
|---|
| 359 | | ... |
|---|
| 360 | | ValidationError: [u'Ensure this value has at least 10 characters.'] |
|---|
| 361 | | >>> f.to_python('1234567890') |
|---|
| 362 | | u'1234567890' |
|---|
| 363 | | >>> f.to_python('1234567890a') |
|---|
| 364 | | u'1234567890a' |
|---|
| 365 | | """ |
|---|
| 366 | | def __init__(self, max_length=None, min_length=None, required=True, widget=None): |
|---|
| 367 | | Field.__init__(self, required, widget) |
|---|
| 368 | | self.max_length, self.min_length = max_length, min_length |
|---|
| 369 | | |
|---|
| 370 | | def to_python(self, value): |
|---|
| 371 | | "Validates max_length and min_length. Returns a Unicode object." |
|---|
| 372 | | Field.to_python(self, value) |
|---|
| 373 | | if value in EMPTY_VALUES: value = u'' |
|---|
| 374 | | if not isinstance(value, basestring): |
|---|
| 375 | | value = unicode(str(value), DEFAULT_ENCODING) |
|---|
| 376 | | elif not isinstance(value, unicode): |
|---|
| 377 | | value = unicode(value, DEFAULT_ENCODING) |
|---|
| 378 | | if self.max_length is not None and len(value) > self.max_length: |
|---|
| 379 | | raise ValidationError(u'Ensure this value has at most %d characters.' % self.max_length) |
|---|
| 380 | | if self.min_length is not None and len(value) < self.min_length: |
|---|
| 381 | | raise ValidationError(u'Ensure this value has at least %d characters.' % self.min_length) |
|---|
| 382 | | return value |
|---|
| 383 | | |
|---|
| 384 | | class IntegerField(Field): |
|---|
| 385 | | """ |
|---|
| 386 | | >>> f = IntegerField() |
|---|
| 387 | | >>> f.to_python('1') |
|---|
| 388 | | 1 |
|---|
| 389 | | >>> isinstance(f.to_python('1'), int) |
|---|
| 390 | | True |
|---|
| 391 | | >>> f.to_python('23') |
|---|
| 392 | | 23 |
|---|
| 393 | | >>> f.to_python('a') |
|---|
| 394 | | Traceback (most recent call last): |
|---|
| 395 | | ... |
|---|
| 396 | | ValidationError: [u'Enter a whole number.'] |
|---|
| 397 | | >>> f.to_python('1 ') |
|---|
| 398 | | 1 |
|---|
| 399 | | >>> f.to_python(' 1') |
|---|
| 400 | | 1 |
|---|
| 401 | | >>> f.to_python(' 1 ') |
|---|
| 402 | | 1 |
|---|
| 403 | | >>> f.to_python('1a') |
|---|
| 404 | | Traceback (most recent call last): |
|---|
| 405 | | ... |
|---|
| 406 | | ValidationError: [u'Enter a whole number.'] |
|---|
| 407 | | """ |
|---|
| 408 | | def to_python(self, value): |
|---|
| 409 | | """ |
|---|
| 410 | | Validates that int() can be called on the input. Returns the result |
|---|
| 411 | | of int(). |
|---|
| 412 | | """ |
|---|
| 413 | | super(IntegerField, self).to_python(value) |
|---|
| 414 | | try: |
|---|
| 415 | | return int(value) |
|---|
| 416 | | except (ValueError, TypeError): |
|---|
| 417 | | raise ValidationError(u'Enter a whole number.') |
|---|
| 418 | | |
|---|
| 419 | | DEFAULT_DATE_INPUT_FORMATS = ( |
|---|
| 420 | | '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' |
|---|
| 421 | | '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006' |
|---|
| 422 | | '%d %b %Y', '%d %b, %Y', # '25 Oct 2006', '25 Oct, 2006' |
|---|
| 423 | | '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006' |
|---|
| 424 | | '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006' |
|---|
| 425 | | ) |
|---|
| 426 | | |
|---|
| 427 | | class DateField(Field): |
|---|
| 428 | | """ |
|---|
| 429 | | >>> import datetime |
|---|
| 430 | | >>> f = DateField() |
|---|
| 431 | | >>> f.to_python(datetime.date(2006, 10, 25)) |
|---|
| 432 | | datetime.date(2006, 10, 25) |
|---|
| 433 | | >>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30)) |
|---|
| 434 | | datetime.date(2006, 10, 25) |
|---|
| 435 | | >>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30, 59)) |
|---|
| 436 | | datetime.date(2006, 10, 25) |
|---|
| 437 | | >>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) |
|---|
| 438 | | datetime.date(2006, 10, 25) |
|---|
| 439 | | >>> f.to_python('2006-10-25') |
|---|
| 440 | | datetime.date(2006, 10, 25) |
|---|
| 441 | | >>> f.to_python('10/25/2006') |
|---|
| 442 | | datetime.date(2006, 10, 25) |
|---|
| 443 | | >>> f.to_python('10/25/06') |
|---|
| 444 | | datetime.date(2006, 10, 25) |
|---|
| 445 | | >>> f.to_python('Oct 25 2006') |
|---|
| 446 | | datetime.date(2006, 10, 25) |
|---|
| 447 | | >>> f.to_python('October 25 2006') |
|---|
| 448 | | datetime.date(2006, 10, 25) |
|---|
| 449 | | >>> f.to_python('October 25, 2006') |
|---|
| 450 | | datetime.date(2006, 10, 25) |
|---|
| 451 | | >>> f.to_python('25 October 2006') |
|---|
| 452 | | datetime.date(2006, 10, 25) |
|---|
| 453 | | >>> f.to_python('25 October, 2006') |
|---|
| 454 | | datetime.date(2006, 10, 25) |
|---|
| 455 | | >>> f.to_python('2006-4-31') |
|---|
| 456 | | Traceback (most recent call last): |
|---|
| 457 | | ... |
|---|
| 458 | | ValidationError: [u'Enter a valid date.'] |
|---|
| 459 | | >>> f.to_python('200a-10-25') |
|---|
| 460 | | Traceback (most recent call last): |
|---|
| 461 | | ... |
|---|
| 462 | | ValidationError: [u'Enter a valid date.'] |
|---|
| 463 | | >>> f.to_python('25/10/06') |
|---|
| 464 | | Traceback (most recent call last): |
|---|
| 465 | | ... |
|---|
| 466 | | ValidationError: [u'Enter a valid date.'] |
|---|
| 467 | | >>> f.to_python(None) |
|---|
| 468 | | Traceback (most recent call last): |
|---|
| 469 | | ... |
|---|
| 470 | | ValidationError: [u'This field is required.'] |
|---|
| 471 | | |
|---|
| 472 | | >>> f = DateField(required=False) |
|---|
| 473 | | >>> f.to_python(None) |
|---|
| 474 | | >>> repr(f.to_python(None)) |
|---|
| 475 | | 'None' |
|---|
| 476 | | >>> f.to_python('') |
|---|
| 477 | | >>> repr(f.to_python('')) |
|---|
| 478 | | 'None' |
|---|
| 479 | | |
|---|
| 480 | | DateField accepts an optional input_formats parameter: |
|---|
| 481 | | >>> f = DateField(input_formats=['%Y %m %d']) |
|---|
| 482 | | >>> f.to_python(datetime.date(2006, 10, 25)) |
|---|
| 483 | | datetime.date(2006, 10, 25) |
|---|
| 484 | | >>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30)) |
|---|
| 485 | | datetime.date(2006, 10, 25) |
|---|
| 486 | | >>> f.to_python('2006 10 25') |
|---|
| 487 | | datetime.date(2006, 10, 25) |
|---|
| 488 | | |
|---|
| 489 | | The input_formats parameter overrides all default input formats, |
|---|
| 490 | | so the default formats won't work unless you specify them: |
|---|
| 491 | | >>> f.to_python('2006-10-25') |
|---|
| 492 | | Traceback (most recent call last): |
|---|
| 493 | | ... |
|---|
| 494 | | ValidationError: [u'Enter a valid date.'] |
|---|
| 495 | | >>> f.to_python('10/25/2006') |
|---|
| 496 | | Traceback (most recent call last): |
|---|
| 497 | | ... |
|---|
| 498 | | ValidationError: [u'Enter a valid date.'] |
|---|
| 499 | | >>> f.to_python('10/25/06') |
|---|
| 500 | | Traceback (most recent call last): |
|---|
| 501 | | ... |
|---|
| 502 | | ValidationError: [u'Enter a valid date.'] |
|---|
| 503 | | """ |
|---|
| 504 | | def __init__(self, input_formats=None, required=True, widget=None): |
|---|
| 505 | | Field.__init__(self, required, widget) |
|---|
| 506 | | self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS |
|---|
| 507 | | |
|---|
| 508 | | def to_python(self, value): |
|---|
| 509 | | """ |
|---|
| 510 | | Validates that the input can be converted to a date. Returns a Python |
|---|
| 511 | | datetime.date object. |
|---|
| 512 | | """ |
|---|
| 513 | | Field.to_python(self, value) |
|---|
| 514 | | if value in EMPTY_VALUES: |
|---|
| 515 | | return None |
|---|
| 516 | | if isinstance(value, datetime.datetime): |
|---|
| 517 | | return value.date() |
|---|
| 518 | | if isinstance(value, datetime.date): |
|---|
| 519 | | return value |
|---|
| 520 | | for format in self.input_formats: |
|---|
| 521 | | try: |
|---|
| 522 | | return datetime.date(*time.strptime(value, format)[:3]) |
|---|
| 523 | | except ValueError: |
|---|
| 524 | | continue |
|---|
| 525 | | raise ValidationError(u'Enter a valid date.') |
|---|
| 526 | | |
|---|
| 527 | | DEFAULT_DATETIME_INPUT_FORMATS = ( |
|---|
| 528 | | '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' |
|---|
| 529 | | '%Y-%m-%d %H:%M', # '2006-10-25 14:30' |
|---|
| 530 | | '%Y-%m-%d', # '2006-10-25' |
|---|
| 531 | | '%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59' |
|---|
| 532 | | '%m/%d/%Y %H:%M', # '10/25/2006 14:30' |
|---|
| 533 | | '%m/%d/%Y', # '10/25/2006' |
|---|
| 534 | | '%m/%d/%y %H:%M:%S', # '10/25/06 14:30:59' |
|---|
| 535 | | '%m/%d/%y %H:%M', # '10/25/06 14:30' |
|---|
| 536 | | '%m/%d/%y', # '10/25/06' |
|---|
| 537 | | ) |
|---|
| 538 | | |
|---|
| 539 | | class DateTimeField(Field): |
|---|
| 540 | | """ |
|---|
| 541 | | >>> import datetime |
|---|
| 542 | | >>> f = DateTimeField() |
|---|
| 543 | | >>> f.to_python(datetime.date(2006, 10, 25)) |
|---|
| 544 | | datetime.datetime(2006, 10, 25, 0, 0) |
|---|
| 545 | | >>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30)) |
|---|
| 546 | | datetime.datetime(2006, 10, 25, 14, 30) |
|---|
| 547 | | >>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30, 59)) |
|---|
| 548 | | datetime.datetime(2006, 10, 25, 14, 30, 59) |
|---|
| 549 | | >>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) |
|---|
| 550 | | datetime.datetime(2006, 10, 25, 14, 30, 59, 200) |
|---|
| 551 | | >>> f.to_python('2006-10-25 14:30:45') |
|---|
| 552 | | datetime.datetime(2006, 10, 25, 14, 30, 45) |
|---|
| 553 | | >>> f.to_python('2006-10-25 14:30:00') |
|---|
| 554 | | datetime.datetime(2006, 10, 25, 14, 30) |
|---|
| 555 | | >>> f.to_python('2006-10-25 14:30') |
|---|
| 556 | | datetime.datetime(2006, 10, 25, 14, 30) |
|---|
| 557 | | >>> f.to_python('2006-10-25') |
|---|
| 558 | | datetime.datetime(2006, 10, 25, 0, 0) |
|---|
| 559 | | >>> f.to_python('10/25/2006 14:30:45') |
|---|
| 560 | | datetime.datetime(2006, 10, 25, 14, 30, 45) |
|---|
| 561 | | >>> f.to_python('10/25/2006 14:30:00') |
|---|
| 562 | | datetime.datetime(2006, 10, 25, 14, 30) |
|---|
| 563 | | >>> f.to_python('10/25/2006 14:30') |
|---|
| 564 | | datetime.datetime(2006, 10, 25, 14, 30) |
|---|
| 565 | | >>> f.to_python('10/25/2006') |
|---|
| 566 | | datetime.datetime(2006, 10, 25, 0, 0) |
|---|
| 567 | | >>> f.to_python('10/25/06 14:30:45') |
|---|
| 568 | | datetime.datetime(2006, 10, 25, 14, 30, 45) |
|---|
| 569 | | >>> f.to_python('10/25/06 14:30:00') |
|---|
| 570 | | datetime.datetime(2006, 10, 25, 14, 30) |
|---|
| 571 | | >>> f.to_python('10/25/06 14:30') |
|---|
| 572 | | datetime.datetime(2006, 10, 25, 14, 30) |
|---|
| 573 | | >>> f.to_python('10/25/06') |
|---|
| 574 | | datetime.datetime(2006, 10, 25, 0, 0) |
|---|
| 575 | | >>> f.to_python('hello') |
|---|
| 576 | | Traceback (most recent call last): |
|---|
| 577 | | ... |
|---|
| 578 | | ValidationError: [u'Enter a valid date/time.'] |
|---|
| 579 | | >>> f.to_python('2006-10-25 4:30 p.m.') |
|---|
| 580 | | Traceback (most recent call last): |
|---|
| 581 | | ... |
|---|
| 582 | | ValidationError: [u'Enter a valid date/time.'] |
|---|
| 583 | | |
|---|
| 584 | | DateField accepts an optional input_formats parameter: |
|---|
| 585 | | >>> f = DateTimeField(input_formats=['%Y %m %d %I:%M %p']) |
|---|
| 586 | | >>> f.to_python(datetime.date(2006, 10, 25)) |
|---|
| 587 | | datetime.datetime(2006, 10, 25, 0, 0) |
|---|
| 588 | | >>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30)) |
|---|
| 589 | | datetime.datetime(2006, 10, 25, 14, 30) |
|---|
| 590 | | >>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30, 59)) |
|---|
| 591 | | datetime.datetime(2006, 10, 25, 14, 30, 59) |
|---|
| 592 | | >>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) |
|---|
| 593 | | datetime.datetime(2006, 10, 25, 14, 30, 59, 200) |
|---|
| 594 | | >>> f.to_python('2006 10 25 2:30 PM') |
|---|
| 595 | | datetime.datetime(2006, 10, 25, 14, 30) |
|---|
| 596 | | |
|---|
| 597 | | The input_formats parameter overrides all default input formats, |
|---|
| 598 | | so the default formats won't work unless you specify them: |
|---|
| 599 | | >>> f.to_python('2006-10-25 14:30:45') |
|---|
| 600 | | Traceback (most recent call last): |
|---|
| 601 | | ... |
|---|
| 602 | | ValidationError: [u'Enter a valid date/time.'] |
|---|
| 603 | | """ |
|---|
| 604 | | def __init__(self, input_formats=None, required=True, widget=None): |
|---|
| 605 | | Field.__init__(self, required, widget) |
|---|
| 606 | | self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS |
|---|
| 607 | | |
|---|
| 608 | | def to_python(self, value): |
|---|
| 609 | | """ |
|---|
| 610 | | Validates that the input can be converted to a datetime. Returns a |
|---|
| 611 | | Python datetime.datetime object. |
|---|
| 612 | | """ |
|---|
| 613 | | Field.to_python(self, value) |
|---|
| 614 | | if value in EMPTY_VALUES: |
|---|
| 615 | | return None |
|---|
| 616 | | if isinstance(value, datetime.datetime): |
|---|
| 617 | | return value |
|---|
| 618 | | if isinstance(value, datetime.date): |
|---|
| 619 | | return datetime.datetime(value.year, value.month, value.day) |
|---|
| 620 | | for format in self.input_formats: |
|---|
| 621 | | try: |
|---|
| 622 | | return datetime.datetime(*time.strptime(value, format)[:6]) |
|---|
| 623 | | except ValueError: |
|---|
| 624 | | continue |
|---|
| 625 | | raise ValidationError(u'Enter a valid date/time.') |
|---|
| 626 | | |
|---|
| 627 | | class RegexField(Field): |
|---|
| 628 | | """ |
|---|
| 629 | | >>> import re |
|---|
| 630 | | |
|---|
| 631 | | >>> f = RegexField('^\d[A-F]\d$') |
|---|
| 632 | | >>> f.to_python('2A2') |
|---|
| 633 | | u'2A2' |
|---|
| 634 | | >>> f.to_python('3F3') |
|---|
| 635 | | u'3F3' |
|---|
| 636 | | >>> f.to_python('3G3') |
|---|
| 637 | | Traceback (most recent call last): |
|---|
| 638 | | ... |
|---|
| 639 | | ValidationError: [u'Enter a valid value.'] |
|---|
| 640 | | >>> f.to_python(' 2A2') |
|---|
| 641 | | Traceback (most recent call last): |
|---|
| 642 | | ... |
|---|
| 643 | | ValidationError: [u'Enter a valid value.'] |
|---|
| 644 | | >>> f.to_python('2A2 ') |
|---|
| 645 | | Traceback (most recent call last): |
|---|
| 646 | | ... |
|---|
| 647 | | ValidationError: [u'Enter a valid value.'] |
|---|
| 648 | | |
|---|
| 649 | | Alternatively, RegexField can take a compiled regular expression: |
|---|
| 650 | | >>> f = RegexField(re.compile('^\d[A-F]\d$')) |
|---|
| 651 | | >>> f.to_python('2A2') |
|---|
| 652 | | u'2A2' |
|---|
| 653 | | >>> f.to_python('3F3') |
|---|
| 654 | | u'3F3' |
|---|
| 655 | | >>> f.to_python('3G3') |
|---|
| 656 | | Traceback (most recent call last): |
|---|
| 657 | | ... |
|---|
| 658 | | ValidationError: [u'Enter a valid value.'] |
|---|
| 659 | | >>> f.to_python(' 2A2') |
|---|
| 660 | | Traceback (most recent call last): |
|---|
| 661 | | ... |
|---|
| 662 | | ValidationError: [u'Enter a valid value.'] |
|---|
| 663 | | >>> f.to_python('2A2 ') |
|---|
| 664 | | Traceback (most recent call last): |
|---|
| 665 | | ... |
|---|
| 666 | | ValidationError: [u'Enter a valid value.'] |
|---|
| 667 | | |
|---|
| 668 | | RegexField takes an optional error_message argument: |
|---|
| 669 | | >>> f = RegexField('^\d\d\d\d$', 'Enter a four-digit number.') |
|---|
| 670 | | >>> f.to_python('1234') |
|---|
| 671 | | u'1234' |
|---|
| 672 | | >>> f.to_python('123') |
|---|
| 673 | | Traceback (most recent call last): |
|---|
| 674 | | ... |
|---|
| 675 | | ValidationError: [u'Enter a four-digit number.'] |
|---|
| 676 | | >>> f.to_python('abcd') |
|---|
| 677 | | Traceback (most recent call last): |
|---|
| 678 | | ... |
|---|
| 679 | | ValidationError: [u'Enter a four-digit number.'] |
|---|
| 680 | | """ |
|---|
| 681 | | def __init__(self, regex, error_message=None, required=True, widget=None): |
|---|
| 682 | | """ |
|---|
| 683 | | regex can be either a string or a compiled regular expression object. |
|---|
| 684 | | error_message is an optional error message to use, if |
|---|
| 685 | | 'Enter a valid value' is too generic for you. |
|---|
| 686 | | """ |
|---|
| 687 | | Field.__init__(self, required, widget) |
|---|
| 688 | | if isinstance(regex, basestring): |
|---|
| 689 | | regex = re.compile(regex) |
|---|
| 690 | | self.regex = regex |
|---|
| 691 | | self.error_message = error_message or u'Enter a valid value.' |
|---|
| 692 | | |
|---|
| 693 | | def to_python(self, value): |
|---|
| 694 | | """ |
|---|
| 695 | | Validates that the input matches the regular expression. Returns a |
|---|
| 696 | | Unicode object. |
|---|
| 697 | | """ |
|---|
| 698 | | Field.to_python(self, value) |
|---|
| 699 | | if value in EMPTY_VALUES: value = u'' |
|---|
| 700 | | if not isinstance(value, basestring): |
|---|
| 701 | | value = unicode(str(value), DEFAULT_ENCODING) |
|---|
| 702 | | elif not isinstance(value, unicode): |
|---|
| 703 | | value = unicode(value, DEFAULT_ENCODING) |
|---|
| 704 | | if not self.regex.search(value): |
|---|
| 705 | | raise ValidationError(self.error_message) |
|---|
| 706 | | return value |
|---|
| 707 | | |
|---|
| 708 | | email_re = re.compile( |
|---|
| 709 | | r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom |
|---|
| 710 | | r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string |
|---|
| 711 | | r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain |
|---|
| 712 | | |
|---|
| 713 | | class EmailField(RegexField): |
|---|
| 714 | | """ |
|---|
| 715 | | >>> f = EmailField() |
|---|
| 716 | | >>> f.to_python('person@example.com') |
|---|
| 717 | | u'person@example.com' |
|---|
| 718 | | >>> f.to_python('foo') |
|---|
| 719 | | Traceback (most recent call last): |
|---|
| 720 | | ... |
|---|
| 721 | | ValidationError: [u'Enter a valid e-mail address.'] |
|---|
| 722 | | >>> f.to_python('foo@') |
|---|
| 723 | | Traceback (most recent call last): |
|---|
| 724 | | ... |
|---|
| 725 | | ValidationError: [u'Enter a valid e-mail address.'] |
|---|
| 726 | | >>> f.to_python('foo@bar') |
|---|
| 727 | | Traceback (most recent call last): |
|---|
| 728 | | ... |
|---|
| 729 | | ValidationError: [u'Enter a valid e-mail address.'] |
|---|
| 730 | | """ |
|---|
| 731 | | def __init__(self, required=True, widget=None): |
|---|
| 732 | | RegexField.__init__(self, email_re, u'Enter a valid e-mail address.', required, widget) |
|---|
| 733 | | |
|---|
| 734 | | class BooleanField(Field): |
|---|
| 735 | | """ |
|---|
| 736 | | >>> f = BooleanField() |
|---|
| 737 | | >>> f.to_python(True) |
|---|
| 738 | | True |
|---|
| 739 | | >>> f.to_python(False) |
|---|
| 740 | | False |
|---|
| 741 | | >>> f.to_python(1) |
|---|
| 742 | | True |
|---|
| 743 | | >>> f.to_python(0) |
|---|
| 744 | | False |
|---|
| 745 | | >>> f.to_python('Django rocks') |
|---|
| 746 | | True |
|---|
| 747 | | """ |
|---|
| 748 | | widget = CheckboxInput |
|---|
| 749 | | |
|---|
| 750 | | def to_python(self, value): |
|---|
| 751 | | "Returns a Python boolean object." |
|---|
| 752 | | Field.to_python(self, value) |
|---|
| 753 | | return bool(value) |
|---|
| 754 | | |
|---|
| 755 | | ######### |
|---|
| 756 | | # FORMS # |
|---|
| 757 | | ######### |
|---|
| 758 | | |
|---|
| 759 | | class DeclarativeFieldsMetaclass(type): |
|---|
| 760 | | "Metaclass that converts Field attributes to a dictionary called 'fields'." |
|---|
| 761 | | def __new__(cls, name, bases, attrs): |
|---|
| 762 | | attrs['fields'] = dict([(name, attrs.pop(name)) for name, obj in attrs.items() if isinstance(obj, Field)]) |
|---|
| 763 | | return type.__new__(cls, name, bases, attrs) |
|---|
| 764 | | |
|---|
| 765 | | class Form(object): |
|---|
| 766 | | "A collection of Fields, plus their associated data." |
|---|
| 767 | | __metaclass__ = DeclarativeFieldsMetaclass |
|---|
| 768 | | |
|---|
| 769 | | def __init__(self, data=None): # TODO: prefix stuff |
|---|
| 770 | | self.data = data or {} |
|---|
| 771 | | self.__data_python = None # Stores the data after to_python() has been called. |
|---|
| 772 | | self.__errors = None # Stores the errors after to_python() has been called. |
|---|
| 773 | | |
|---|
| 774 | | def __iter__(self): |
|---|
| 775 | | for name, field in self.fields.items(): |
|---|
| 776 | | yield BoundField(self, field, name) |
|---|
| 777 | | |
|---|
| 778 | | def to_python(self): |
|---|
| 779 | | if self.__errors is None: |
|---|
| 780 | | self._validate() |
|---|
| 781 | | return self.__data_python |
|---|
| 782 | | |
|---|
| 783 | | def errors(self): |
|---|
| 784 | | "Returns an ErrorDict for self.data" |
|---|
| 785 | | if self.__errors is None: |
|---|
| 786 | | self._validate() |
|---|
| 787 | | return self.__errors |
|---|
| 788 | | |
|---|
| 789 | | def is_valid(self): |
|---|
| 790 | | """ |
|---|
| 791 | | Returns True if the form has no errors. Otherwise, False. This exists |
|---|
| 792 | | solely for convenience, so client code can use positive logic rather |
|---|
| 793 | | than confusing negative logic ("if not form.errors()"). |
|---|
| 794 | | """ |
|---|
| 795 | | return not bool(self.errors()) |
|---|
| 796 | | |
|---|
| 797 | | def __getitem__(self, name): |
|---|
| 798 | | "Returns a BoundField with the given name." |
|---|
| 799 | | try: |
|---|
| 800 | | field = self.fields[name] |
|---|
| 801 | | except KeyError: |
|---|
| 802 | | raise KeyError('Key %r not found in Form' % name) |
|---|
| 803 | | return BoundField(self, field, name) |
|---|
| 804 | | |
|---|
| 805 | | def _validate(self): |
|---|
| 806 | | data_python = {} |
|---|
| 807 | | errors = ErrorDict() |
|---|
| 808 | | for name, field in self.fields.items(): |
|---|
| 809 | | try: |
|---|
| 810 | | value = field.to_python(self.data.get(name, None)) |
|---|
| 811 | | data_python[name] = value |
|---|
| 812 | | except ValidationError, e: |
|---|
| 813 | | errors[name] = e.messages |
|---|
| 814 | | if not errors: # Only set self.data_python if there weren't errors. |
|---|
| 815 | | self.__data_python = data_python |
|---|
| 816 | | self.__errors = errors |
|---|
| 817 | | |
|---|
| 818 | | class BoundField(object): |
|---|
| 819 | | "A Field plus data" |
|---|
| 820 | | def __init__(self, form, field, name): |
|---|
| 821 | | self._form = form |
|---|
| 822 | | self._field = field |
|---|
| 823 | | self._name = name |
|---|
| 824 | | |
|---|
| 825 | | def __str__(self): |
|---|
| 826 | | "Renders this field as an HTML widget." |
|---|
| 827 | | # Use the 'widget' attribute on the field to determine which type |
|---|
| 828 | | # of HTML widget to use. |
|---|
| 829 | | return self.as_widget(self._field.widget) |
|---|
| 830 | | |
|---|
| 831 | | def _errors(self): |
|---|
| 832 | | """ |
|---|
| 833 | | Returns an ErrorList for this field. Returns an empty ErrorList |
|---|
| 834 | | if there are none. |
|---|
| 835 | | """ |
|---|
| 836 | | try: |
|---|
| 837 | | return self._form.errors()[self._name] |
|---|
| 838 | | except KeyError: |
|---|
| 839 | | return ErrorList() |
|---|
| 840 | | errors = property(_errors) |
|---|
| 841 | | |
|---|
| 842 | | def as_widget(self, widget, attrs=None): |
|---|
| 843 | | return widget.render(self._name, self._form.data.get(self._name, None), attrs=attrs) |
|---|
| 844 | | |
|---|
| 845 | | def as_text(self, attrs=None): |
|---|
| 846 | | """ |
|---|
| 847 | | Returns a string of HTML for representing this as an <input type="text">. |
|---|
| 848 | | """ |
|---|
| 849 | | return self.as_widget(TextInput(), attrs) |
|---|
| 850 | | |
|---|
| 851 | | def as_textarea(self, attrs=None): |
|---|
| 852 | | "Returns a string of HTML for representing this as a <textarea>." |
|---|
| 853 | | return self.as_widget(Textarea(), attrs) |
|---|
| | 15 | from widgets import * |
|---|
| | 16 | from fields import * |
|---|
| | 17 | from forms import Form |
|---|