Package muntjac :: Package ui :: Module date_field
[hide private]
[frames] | no frames]

Source Code for Module muntjac.ui.date_field

  1  # Copyright (C) 2012 Vaadin Ltd.  
  2  # Copyright (C) 2012 Richard Lincoln 
  3  #  
  4  # Licensed under the Apache License, Version 2.0 (the "License");  
  5  # you may not use this file except in compliance with the License.  
  6  # You may obtain a copy of the License at  
  7  #  
  8  #     http://www.apache.org/licenses/LICENSE-2.0  
  9  #  
 10  # Unless required by applicable law or agreed to in writing, software  
 11  # distributed under the License is distributed on an "AS IS" BASIS,  
 12  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
 13  # See the License for the specific language governing permissions and  
 14  # limitations under the License. 
 15   
 16  """Defines a date editor component.""" 
 17   
 18  from datetime import datetime 
 19   
 20  from babel.dates import parse_date 
 21   
 22  from muntjac.ui.abstract_field import AbstractField 
 23  from muntjac.data.property import IProperty, ConversionException 
 24  from muntjac.ui.form import Form 
 25  from muntjac.data.validator import InvalidValueException 
 26  from muntjac.terminal.gwt.client.ui.v_date_field import VDateField 
 27   
 28  from muntjac.event.field_events import \ 
 29      (BlurEvent, IBlurListener, IBlurNotifier, FocusEvent, IFocusListener, 
 30       IFocusNotifier) 
 31   
 32  from muntjac.util import totalseconds 
 33   
 34   
35 -class DateField(AbstractField, IBlurNotifier, IFocusNotifier):
36 """A date editor component that can be bound to any L{IProperty} 37 that is compatible with C{datetime}. 38 39 Since C{DateField} extends C{AbstractField} it implements the L{IBuffered} 40 interface. 41 42 A C{DateField} is in write-through mode by default, so 43 L{AbstractField.setWriteThrough} must be called to enable buffering. 44 45 @author: Vaadin Ltd. 46 @author: Richard Lincoln 47 @version: 1.1.2 48 """ 49 50 CLIENT_WIDGET = None #ClientWidget(VPopupCalendar, LoadStyle.EAGER) 51 52 #: Resolution identifier: milliseconds. 53 RESOLUTION_MSEC = 0 54 55 #: Resolution identifier: seconds. 56 RESOLUTION_SEC = 1 57 58 #: Resolution identifier: minutes. 59 RESOLUTION_MIN = 2 60 61 #: Resolution identifier: hours. 62 RESOLUTION_HOUR = 3 63 64 #: Resolution identifier: days. 65 RESOLUTION_DAY = 4 66 67 #: Resolution identifier: months. 68 RESOLUTION_MONTH = 5 69 70 #: Resolution identifier: years. 71 RESOLUTION_YEAR = 6 72 73 #: Specified largest modifiable unit. 74 _largestModifiable = RESOLUTION_YEAR 75
76 - def __init__(self, *args):
77 """Constructs an new C{DateField}. 78 79 @param args: tuple of the form 80 - () 81 - (caption) 82 1. the caption of the datefield. 83 - (caption, dataSource) 84 1. the caption string for the editor. 85 2. the IProperty to be edited with this editor. 86 - (dataSource) 87 1. the IProperty to be edited with this editor. 88 - (caption, value) 89 1. the caption string for the editor. 90 2. the date value. 91 """ 92 super(DateField, self).__init__() 93 94 #: The internal calendar to be used in java.utl.Date conversions. 95 self._calendar = None 96 97 #: Overridden format string 98 self._dateFormat = None 99 100 self._lenient = False 101 102 self._dateString = None 103 104 #: Was the last entered string parsable? If this flag is false, 105 # datefields internal validator does not pass. 106 self._uiHasValidDateString = True 107 108 #: Determines if week numbers are shown in the date selector. 109 self._showISOWeekNumbers = False 110 111 self._currentParseErrorMessage = None 112 113 self._defaultParseErrorMessage = 'Date format not recognized' 114 115 self._timeZone = None 116 117 #: Specified smallest modifiable unit. 118 self._resolution = self.RESOLUTION_MSEC 119 120 nargs = len(args) 121 if nargs == 0: 122 pass 123 elif nargs == 1: 124 if isinstance(args[0], IProperty): 125 dataSource, = args 126 if not issubclass(dataSource.getType(), datetime): 127 raise ValueError, ('Can\'t use ' 128 + dataSource.getType().__name__ 129 + ' typed property as datasource') 130 self.setPropertyDataSource(dataSource) 131 else: 132 caption, = args 133 self.setCaption(caption) 134 elif nargs == 2: 135 if isinstance(args[1], datetime): 136 caption, value = args 137 self.setValue(value) 138 self.setCaption(caption) 139 else: 140 caption, dataSource = args 141 DateField.__init__(self, dataSource) 142 self.setCaption(caption) 143 else: 144 raise ValueError, 'too many arguments'
145 146
147 - def __getstate__(self):
148 result = self.__dict__.copy() 149 del result['_calendar'] 150 return result
151 152
153 - def __setstate__(self, d):
154 self.__dict__ = d 155 self._calendar = None
156 157
158 - def paintContent(self, target):
159 # Paints this component. 160 super(DateField, self).paintContent(target) 161 162 # Adds the locale as attribute 163 l = self.getLocale() 164 if l is not None: 165 target.addAttribute('locale', str(l)) 166 167 if self.getDateFormat() is not None: 168 target.addAttribute('format', self._dateFormat) 169 170 if not self.isLenient(): 171 target.addAttribute('strict', True) 172 173 target.addAttribute(VDateField.WEEK_NUMBERS, 174 self.isShowISOWeekNumbers()) 175 target.addAttribute('parsable', self._uiHasValidDateString) 176 177 # TODO communicate back the invalid date string? E.g. returning 178 # back to app or refresh. 179 180 # Gets the calendar 181 calendar = self.getCalendar() 182 currentDate = self.getValue() 183 184 r = self._resolution 185 while r <= self._largestModifiable: 186 187 t = -1 188 if r == self.RESOLUTION_MSEC: 189 if currentDate is not None: 190 t = calendar.microsecond / 1e03 191 target.addVariable(self, 'msec', t) 192 193 elif r == self.RESOLUTION_SEC: 194 if currentDate is not None: 195 t = calendar.second 196 target.addVariable(self, 'sec', t) 197 198 elif r == self.RESOLUTION_MIN: 199 if currentDate is not None: 200 t = calendar.minute 201 target.addVariable(self, 'min', t) 202 203 elif r == self.RESOLUTION_HOUR: 204 if currentDate is not None: 205 t = calendar.hour 206 target.addVariable(self, 'hour', t) 207 208 elif r == self.RESOLUTION_DAY: 209 if currentDate is not None: 210 t = calendar.day 211 target.addVariable(self, 'day', t) 212 213 elif r == self.RESOLUTION_MONTH: 214 if currentDate is not None: 215 t = calendar.month# + 1 216 target.addVariable(self, 'month', t) 217 218 elif r == self.RESOLUTION_YEAR: 219 if currentDate is not None: 220 t = calendar.year 221 target.addVariable(self, 'year', t) 222 223 r += 1
224 225
226 - def shouldHideErrors(self):
227 shouldHideErrors = super(DateField, self).shouldHideErrors() 228 return shouldHideErrors and self._uiHasValidDateString
229 230
231 - def changeVariables(self, source, variables):
232 # Invoked when a variable of the component changes. 233 super(DateField, self).changeVariables(source, variables) 234 235 if (not self.isReadOnly() 236 and ('year' in variables 237 or 'month' in variables 238 or 'day' in variables 239 or 'hour' in variables 240 or 'min' in variables 241 or 'sec' in variables 242 or 'msec' in variables 243 or 'dateString' in variables)): 244 # Old and new dates 245 oldDate = self.getValue() 246 newDate = None 247 248 # this enables analyzing invalid input on the server 249 newDateString = variables.get('dateString') 250 self._dateString = newDateString 251 252 # Gets the new date in parts 253 # Null values are converted to negative values. 254 year = -1 if variables.get('year') is None else int(variables.get('year')) if 'year' in variables else -1 255 month = -1 if variables.get('month') is None else int(variables.get('month')) if 'month' in variables else -1 256 day = -1 if variables.get('day') is None else int(variables.get('day')) if 'day' in variables else -1 257 hour = -1 if variables.get('hour') is None else int(variables.get('hour')) if 'hour' in variables else -1 258 minn = -1 if variables.get('min') is None else int(variables.get('min')) if 'min' in variables else -1 259 sec = -1 if variables.get('sec') is None else int(variables.get('sec')) if 'sec' in variables else -1 260 msec = -1 if variables.get('msec') is None else int(variables.get('msec')) if 'msec' in variables else -1 261 262 # If all of the components is < 0 use the previous value 263 if (year < 0 and month < 0 and day < 0 and hour < 0 and min < 0 264 and sec < 0 and msec < 0): 265 newDate = None 266 else: 267 # Clone the calendar for date operation 268 cal = self.getCalendar() 269 270 # Make sure that meaningful values exists 271 # Use the previous value if some of the variables 272 # have not been changed. 273 year = cal.year if year < 0 else year 274 month = cal.month if month < 0 else month 275 day = cal.day if day < 0 else day 276 hour = cal.hour if hour < 0 else hour 277 minn = cal.minute if minn < 0 else minn 278 sec = cal.second if sec < 0 else sec 279 msec = cal.microsecond * 1e03 if msec < 0 else msec 280 281 # Sets the calendar fields 282 cal = datetime(year, month, day, hour, minn, sec, int(msec / 1e03)) 283 284 # Assigns the date 285 newDate = cal #totalseconds(cal - datetime(1970, 1, 1)) 286 #newDate = mktime(cal.timetuple()) 287 288 if (newDate is None and self._dateString is not None 289 and '' != self._dateString): 290 try: 291 parsedDate = \ 292 self.handleUnparsableDateString(self._dateString) 293 self.setValue(parsedDate, True) 294 295 # Ensure the value is sent to the client if the value is 296 # set to the same as the previous (#4304). Does not 297 # repaint if handleUnparsableDateString throws an 298 # exception. In this case the invalid text remains in the 299 # DateField. 300 self.requestRepaint() 301 except ConversionException, e: 302 303 # Datefield now contains some text that could't be parsed 304 # into date. 305 if oldDate is not None: 306 # Set the logic value to null. 307 self.setValue(None) 308 # Reset the dateString (overridden to null by setValue) 309 self._dateString = newDateString 310 311 # Saves the localized message of parse error. This can be 312 # overridden in handleUnparsableDateString. The message 313 # will later be used to show a validation error. 314 self._currentParseErrorMessage = e.getLocalizedMessage() 315 316 # The value of the DateField should be null if an invalid 317 # value has been given. Not using setValue() since we do 318 # not want to cause the client side value to change. 319 self._uiHasValidDateString = False 320 321 # Because of our custom implementation of isValid(), that 322 # also checks the parsingSucceeded flag, we must also 323 # notify the form (if this is used in one) that the 324 # validity of this field has changed. 325 # 326 # Normally fields validity doesn't change without value 327 # change and form depends on this implementation detail. 328 self.notifyFormOfValidityChange() 329 330 self.requestRepaint() 331 elif (newDate != oldDate 332 and (newDate is None or newDate != oldDate)): 333 self.setValue(newDate, True) 334 # Don't require a repaint, client updates itself 335 elif not self._uiHasValidDateString: 336 # oldDate == 337 # newDate == null 338 # Empty value set, previously contained unparsable date 339 # string, clear related internal fields 340 self.setValue(None) 341 342 if FocusEvent.EVENT_ID in variables: 343 self.fireEvent( FocusEvent(self) ) 344 345 if BlurEvent.EVENT_ID in variables: 346 self.fireEvent( BlurEvent(self) )
347 348
349 - def handleUnparsableDateString(self, dateString):
350 """This method is called to handle a non-empty date string from 351 the client if the client could not parse it as a C{datetime}. 352 353 By default, a C{ConversionException} is thrown, and the 354 current value is not modified. 355 356 This can be overridden to handle conversions, to return null 357 (equivalent to empty input), to throw an exception or to fire 358 an event. 359 360 @raise ConversionException: 361 to keep the old value and indicate an error 362 """ 363 self._currentParseErrorMessage = None 364 raise ConversionException( self.getParseErrorMessage() )
365 366
367 - def getType(self):
368 # Gets the edited property's type. 369 return datetime
370 371
372 - def setValue(self, newValue, repaintIsNotNeeded=False):
373 374 # First handle special case when the client side component have a 375 # date string but value is null (e.g. unparsable date string typed 376 # in by the user). No value changes should happen, but we need to 377 # do some internal housekeeping. 378 if newValue is None and not self._uiHasValidDateString: 379 # Side-effects of setInternalValue clears possible previous 380 # strings and flags about invalid input. 381 self.setInternalValue(None) 382 383 # Due to DateField's special implementation of isValid(), 384 # datefields validity may change although the logical value 385 # does not change. This is an issue for Form which expects that 386 # validity of Fields cannot change unless actual value changes. 387 # 388 # So we check if this field is inside a form and the form has 389 # registered this as a field. In this case we repaint the form. 390 # Without this hacky solution the form might not be able to clean 391 # validation errors etc. We could avoid this by firing an extra 392 # value change event, but feels like at least as bad solution as 393 # this. 394 self.notifyFormOfValidityChange() 395 self.requestRepaint() 396 return 397 398 if newValue is None or isinstance(newValue, datetime): 399 super(DateField, self).setValue(newValue, repaintIsNotNeeded) 400 else: 401 try: 402 app = self.getApplication() 403 if app is not None: 404 l = app.getLocale() 405 406 # Try to parse the given string value to datetime 407 currentTimeZone = self.getTimeZone() 408 if currentTimeZone is not None: 409 currentTimeZone # FIXME: parse according to timezone 410 val = parse_date(str(newValue), locale=l) 411 super(DateField, self).setValue(val, repaintIsNotNeeded) 412 except ValueError: 413 self._uiHasValidDateString = False 414 raise ConversionException(self.getParseErrorMessage())
415 416
418 """Detects if this field is used in a Form (logically) and if so, 419 notifies it (by repainting it) that the validity of this field might 420 have changed. 421 """ 422 parenOfDateField = self.getParent() 423 formFound = False 424 while (parenOfDateField is not None) or formFound: 425 if isinstance(parenOfDateField, Form): 426 f = parenOfDateField 427 visibleItemProperties = f.getItemPropertyIds() 428 for fieldId in visibleItemProperties: 429 field = fieldId 430 if field == self: 431 # this datefield is logically in a form. Do the 432 # same thing as form does in its value change 433 # listener that it registers to all fields. 434 f.requestRepaint() 435 formFound = True 436 break 437 if formFound: 438 break 439 parenOfDateField = parenOfDateField.getParent()
440 441
442 - def setPropertyDataSource(self, newDataSource):
443 """Sets the DateField datasource. Datasource type must assignable 444 to Date. 445 446 @see: L{Viewer.setPropertyDataSource} 447 """ 448 if (newDataSource is None 449 or issubclass(newDataSource.getType(), datetime)): 450 super(DateField, self).setPropertyDataSource(newDataSource) 451 else: 452 raise ValueError, 'DateField only supports datetime properties'
453 454
455 - def setInternalValue(self, newValue):
456 # Also set the internal dateString 457 if newValue is not None: 458 self._dateString = str(newValue) 459 else: 460 self._dateString = None 461 462 if not self._uiHasValidDateString: 463 # clear component error and parsing flag 464 self.setComponentError(None) 465 self._uiHasValidDateString = True 466 self._currentParseErrorMessage = None 467 468 super(DateField, self).setInternalValue(newValue)
469 470
471 - def getResolution(self):
472 """Gets the resolution. 473 """ 474 return self._resolution
475 476
477 - def setResolution(self, resolution):
478 """Sets the resolution of the C{DateField}. 479 480 @param resolution: 481 the resolution to set. 482 """ 483 self._resolution = resolution 484 self.requestRepaint()
485 486
487 - def getCalendar(self):
488 """Returns new instance calendar used in Date conversions. 489 490 Returns new clone of the calendar object initialized using the the 491 current date (if available) 492 493 If this is no calendar is assigned the C{calendar} is used. 494 495 @return: the calendar 496 @see: L{setCalendar} 497 """ 498 # Makes sure we have an calendar instance 499 if self._calendar is None: 500 self._calendar = datetime.now() 501 # Clone the instance 502 timestamp = totalseconds(self._calendar - datetime(1970, 1, 1)) 503 newCal = datetime.fromtimestamp(timestamp) 504 505 # Assigns the current time to calendar. 506 currentDate = self.getValue() 507 if currentDate is not None: 508 newCal = currentDate # FIXME:.setTime(currentDate) 509 510 currentTimeZone = self.getTimeZone() 511 if currentTimeZone is not None: 512 currentTimeZone # FIXME: set calendar timezone 513 514 return newCal
515 516
517 - def setDateFormat(self, dateFormat):
518 """Sets formatting used by some component implementations. 519 520 By default it is encouraged to used default formatting defined 521 by Locale. 522 523 @param dateFormat: the dateFormat to set 524 @see: L{AbstractComponent.setLocale} 525 """ 526 self._dateFormat = dateFormat 527 self.requestRepaint()
528 529
530 - def getDateFormat(self):
531 """Returns a format string used to format date value on client side 532 or null if default formatting from L{IComponent.getLocale} is 533 used. 534 535 @return: the dateFormat 536 """ 537 return self._dateFormat
538 539
540 - def setLenient(self, lenient):
541 """Specifies whether or not date/time interpretation in component is 542 to be lenient. 543 544 @see: L{isLenient} 545 @param lenient: 546 true if the lenient mode is to be turned on; false if it 547 is to be turned off. 548 """ 549 self._lenient = lenient 550 self.requestRepaint()
551 552
553 - def isLenient(self):
554 """Returns whether date/time interpretation is to be lenient. 555 556 @see: L{setLenient} 557 @return: true if the interpretation mode of this calendar is lenient; 558 false otherwise. 559 """ 560 return self._lenient
561 562
563 - def addListener(self, listener, iface=None):
564 if (isinstance(listener, IBlurListener) and 565 (iface is None or issubclass(iface, IBlurListener))): 566 self.registerListener(BlurEvent.EVENT_ID, BlurEvent, 567 listener, IBlurListener.blurMethod) 568 569 if (isinstance(listener, IFocusListener) and 570 (iface is None or issubclass(iface, IFocusListener))): 571 self.registerListener(FocusEvent.EVENT_ID, FocusEvent, 572 listener, IFocusListener.focusMethod) 573 574 super(DateField, self).addListener(listener, iface)
575 576
577 - def addCallback(self, callback, eventType=None, *args):
578 if eventType is None: 579 eventType = callback._eventType 580 581 if issubclass(eventType, BlurEvent): 582 self.registerCallback(BlurEvent, callback, 583 BlurEvent.EVENT_ID, *args) 584 585 elif issubclass(eventType, FocusEvent): 586 self.registerCallback(FocusEvent, callback, 587 FocusEvent.EVENT_ID, *args) 588 else: 589 super(DateField, self).addCallback(callback, eventType, *args)
590 591
592 - def removeListener(self, listener, iface=None):
593 if (isinstance(listener, IBlurListener) and 594 (iface is None or issubclass(iface, IBlurListener))): 595 self.withdrawListener(BlurEvent.EVENT_ID, BlurEvent, listener) 596 597 if (isinstance(listener, IFocusListener) and 598 (iface is None or issubclass(iface, IFocusListener))): 599 self.withdrawListener(FocusEvent.EVENT_ID, FocusEvent, listener) 600 601 super(DateField, self).removeListener(listener, iface)
602 603
604 - def removeCallback(self, callback, eventType=None):
605 if eventType is None: 606 eventType = callback._eventType 607 608 if issubclass(eventType, BlurEvent): 609 self.withdrawCallback(BlurEvent, callback, BlurEvent.EVENT_ID) 610 611 elif issubclass(eventType, FocusEvent): 612 self.withdrawCallback(FocusEvent, callback, FocusEvent.EVENT_ID) 613 614 else: 615 super(DateField, self).removeCallback(callback, eventType)
616 617
618 - def isShowISOWeekNumbers(self):
619 """Checks whether ISO 8601 week numbers are shown in the date 620 selector. 621 622 @return: true if week numbers are shown, false otherwise. 623 """ 624 return self._showISOWeekNumbers
625 626
627 - def setShowISOWeekNumbers(self, showWeekNumbers):
628 """Sets the visibility of ISO 8601 week numbers in the date selector. 629 ISO 8601 defines that a week always starts with a Monday so the week 630 numbers are only shown if this is the case. 631 632 @param showWeekNumbers: 633 true if week numbers should be shown, false otherwise. 634 """ 635 self._showISOWeekNumbers = showWeekNumbers 636 self.requestRepaint()
637 638
639 - def isValid(self):
640 """Tests the current value against registered validators if the field 641 is not empty. Note that DateField is considered empty (value == null) 642 and invalid if it contains text typed in by the user that couldn't be 643 parsed into a Date value. 644 645 @see: L{AbstractField.isValid} 646 """ 647 return self._uiHasValidDateString and super(DateField, self).isValid()
648 649
650 - def validate(self):
651 # To work properly in form we must throw exception if there is 652 # currently a parsing error in the datefield. Parsing error is 653 # kind of an internal validator. 654 if not self._uiHasValidDateString: 655 raise UnparsableDateString(self._currentParseErrorMessage) 656 super(DateField, self).validate()
657 658
659 - def getParseErrorMessage(self):
660 """Return the error message that is shown if the user inputted value 661 can't be parsed into a datetime object. If 662 L{handleUnparsableDateString} is overridden and it 663 throws a custom exception, the message returned by 664 L{Exception.message} will be used instead of the 665 value returned by this method. 666 667 @see: L{setParseErrorMessage} 668 669 @return: the error message that the DateField uses when it can't parse 670 the textual input from user to a Date object 671 """ 672 return self._defaultParseErrorMessage
673 674
675 - def setParseErrorMessage(self, parsingErrorMessage):
676 """Sets the default error message used if the DateField cannot parse 677 the text input by user to a datetime field. Note that if the 678 L{handleUnparsableDateString} method is overridden, the localized 679 message from its exception is used. 680 681 @see: L{getParseErrorMessage} 682 @see: L{handleUnparsableDateString} 683 """ 684 self._defaultParseErrorMessage = parsingErrorMessage
685 686
687 - def setTimeZone(self, timeZone):
688 """Sets the time zone used by this date field. The time zone is used 689 to convert the absolute time in a Date object to a logical time 690 displayed in the selector and to convert the select time back to a 691 datetime object. 692 693 If no time zone has been set, the current default time zone returned 694 by C{TimeZone.getDefault()} is used. 695 696 @see L{getTimeZone()} 697 @param timeZone: 698 the time zone to use for time calculations. 699 """ 700 self._timeZone = timeZone 701 self.requestRepaint()
702 703
704 - def getTimeZone(self):
705 """Gets the time zone used by this field. The time zone is used to 706 convert the absolute time in a Date object to a logical time displayed 707 in the selector and to convert the select time back to a datetime 708 object. 709 710 If {@code null} is returned, the current default time zone returned by 711 C{TimeZone.getDefault()} is used. 712 713 @return: the current time zone 714 """ 715 return self._timeZone
716 717
718 -class UnparsableDateString(InvalidValueException):
719
720 - def __init__(self, message):
721 super(UnparsableDateString, self).__init__(message)
722