1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
51
52
53 RESOLUTION_MSEC = 0
54
55
56 RESOLUTION_SEC = 1
57
58
59 RESOLUTION_MIN = 2
60
61
62 RESOLUTION_HOUR = 3
63
64
65 RESOLUTION_DAY = 4
66
67
68 RESOLUTION_MONTH = 5
69
70
71 RESOLUTION_YEAR = 6
72
73
74 _largestModifiable = RESOLUTION_YEAR
75
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
95 self._calendar = None
96
97
98 self._dateFormat = None
99
100 self._lenient = False
101
102 self._dateString = None
103
104
105
106 self._uiHasValidDateString = True
107
108
109 self._showISOWeekNumbers = False
110
111 self._currentParseErrorMessage = None
112
113 self._defaultParseErrorMessage = 'Date format not recognized'
114
115 self._timeZone = None
116
117
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
148 result = self.__dict__.copy()
149 del result['_calendar']
150 return result
151
152
154 self.__dict__ = d
155 self._calendar = None
156
157
158 - def paintContent(self, target):
159
160 super(DateField, self).paintContent(target)
161
162
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
178
179
180
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
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
229
230
232
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
245 oldDate = self.getValue()
246 newDate = None
247
248
249 newDateString = variables.get('dateString')
250 self._dateString = newDateString
251
252
253
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
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
268 cal = self.getCalendar()
269
270
271
272
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
282 cal = datetime(year, month, day, hour, minn, sec, int(msec / 1e03))
283
284
285 newDate = cal
286
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
296
297
298
299
300 self.requestRepaint()
301 except ConversionException, e:
302
303
304
305 if oldDate is not None:
306
307 self.setValue(None)
308
309 self._dateString = newDateString
310
311
312
313
314 self._currentParseErrorMessage = e.getLocalizedMessage()
315
316
317
318
319 self._uiHasValidDateString = False
320
321
322
323
324
325
326
327
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
335 elif not self._uiHasValidDateString:
336
337
338
339
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
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
370
371
372 - def setValue(self, newValue, repaintIsNotNeeded=False):
415
416
440
441
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
456
457 if newValue is not None:
458 self._dateString = str(newValue)
459 else:
460 self._dateString = None
461
462 if not self._uiHasValidDateString:
463
464 self.setComponentError(None)
465 self._uiHasValidDateString = True
466 self._currentParseErrorMessage = None
467
468 super(DateField, self).setInternalValue(newValue)
469
470
472 """Gets the resolution.
473 """
474 return self._resolution
475
476
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
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
499 if self._calendar is None:
500 self._calendar = datetime.now()
501
502 timestamp = totalseconds(self._calendar - datetime(1970, 1, 1))
503 newCal = datetime.fromtimestamp(timestamp)
504
505
506 currentDate = self.getValue()
507 if currentDate is not None:
508 newCal = currentDate
509
510 currentTimeZone = self.getTimeZone()
511 if currentTimeZone is not None:
512 currentTimeZone
513
514 return newCal
515
516
528
529
538
539
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
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
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
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
616
617
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
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
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
657
658
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
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
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
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
722