"""mems.ui.form.date_widget Provides a date selection widget. """ from types import IntType, NoneType from quixote.form import Widget from quixote.form import FormValueError from mx import DateTime from mems.lib.typeutils import typecheck __revision__ = "$Id: date_widget.ptl,v 1.11 2002/07/09 20:15:39 gward Exp $" __doc__ = """\ DateSelectWidget Quixote date selection widget. 'value' argument is either a DateTime instance or None. The default behavior if 'value' is None, is that the first (and selected option) will be blank (i.e. 'not-selected') and the value returned by the date widget when each of the HTML select components is left in the un-selected state is also None. Otherwise, the select components are populated with the year, month and day corresponding to the DateTime instance specified in value and any changes to the date will be returned as the new value. The constructor for DateSelectWidget takes a 'allow_unselected' keyword argument which is a boolean that is on by default. When turned off, the 'not-selected' option is no longer presented in the HTML. A None 'value' will be turned into a new DateTime instance corresponding to 'now' which then becomes the selected option for the Widget. The constructor for DateSelectWidget also takes a 'use_JS' keyword argument which is a boolean that is on by default. This will construct a short JavaScript function that will be inserted into the HTML (as a comment) that will 'correct' invalid dates on-the-fly, without needing the containing form to be submitted (i.e. 2000-Feb-31 is changed to 2000-Feb-29). Once the form is submitted if the 'auto_correct' boolean keyword argument is true, the same correction is performed regardless of if 'use_JS' was on or not. If 'auto_correct' is false, a FormValueError is raised if the form data contains illegal dates. The exception contains the information that would be used for the correction. Quirks: This widget will currently only work as long as Quixote continues to use the 'one-form-per-page' architecture. If Quixote ever moves to multiple forms/page, then forms will need to be named in the HTML and that name will need to be passed to the constructor of the widget. The 20 line JavaScript function is inserted once, with slight naming variation, for EVERY DateSelectWidget instance :-o So, at the moment, this widget is not recomended for long lists of records on a single page that require date manipulation. Why ? I chose this path because of the initial goal not to change anything in Quixote to support a baseline JavaScript validation. Down the road, I think the *right* way to implement this would be to have some sort of page level (or maybe form level) registration mechanism for adding or inquiring about JS functions that belong to the page, implying a forms rework for Quixote... maybe a future? """ MONTH_NAMES = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') template render_date_select(date, name, year_name, month_name, day_name, allow_unselected, use_JS): # render_date_select(date : DateTime | None, name : string, # year_name : string, month_name : string, # day_name : string, allow_unselected : boolean, # use_JS : boolean) # # Render the HTML for the date_Select widget. If 'allow_unselected' # is true, allow the widget to have a 'blank' option, otherwise # display the date represented by 'date' in three select dialogs. # If 'date' is None, create a new DateTime instance for 'now'. # The HTML is formatted so that it looks decent in 'view source' if date is None: date = DateTime.now() selected = ' selected="selected"' else: selected = "" if allow_unselected: not_selected_option = '' % selected not_selected_size = 1 else: not_selected_option = '' not_selected_size = 0 if use_JS: script_ref = 'onchange="%s_set_day()"' % name datetime_validation_JS(name, year_name, month_name, day_name, not_selected_size) else: script_ref = '' # year select """ """ # month select """ """ # day select """ """ template datetime_validation_JS(name, year_name, month_name, day_name, not_selected_size): # datetime_validation_JS(name : string, year_name : string, # month_name : string, day_name : string, # not_selected_size : int) # # Return the javascript that mimicks the 'datetime_validation' # method from the date_widget in JavaScript. The JS is written # such that it works with the 'render_dateselect' template HTML. # If quixote moves to multiple forms/page, the target form will # probably need a name. Currently quixote forms are not named. # Don't forget to escape the JavaScript mod operator """ """ % (name, year_name, month_name, day_name, 28 + not_selected_size, 27 + not_selected_size, 29 + not_selected_size) class DateSelectWidget (Widget): data_type = "DateTime" widget_type = "date_select" def __init__ (self, name, value=None, allow_unselected=1, use_JS=1, auto_correct=0): typecheck(value, DateTime.DateTimeType, allow_none=1) year_name = name + "_year" # name for the year HTML select month_name = name + "_month" # name for the month HTML select day_name = name + "_day" # name for the day HTML select name = "_".join(name.split('.')) # deal with subwidgets... self.year_name = "_".join(year_name.split('.')) self.month_name = "_".join(month_name.split('.')) self.day_name = "_".join(day_name.split('.')) self.allow_unselected = allow_unselected # add a 'not-selected' option self.auto_correct = auto_correct # correct the date like 'use_JS' does # otherwise raise FormValueError self.use_JS = use_JS # insert Javascript for additional # on-the-fly client-side validation Widget.__init__(self, name, value) def render (self, request): return render_date_select(self.value, self.name, self.year_name, self.month_name, self.day_name, self.allow_unselected, self.use_JS) def parse (self, request): year = request.form.get(self.year_name) mon = request.form.get(self.month_name) day = request.form.get(self.day_name) # look for the 'not-selected' option if not year and not mon and not day: return None # user has tried to select a date try: int_year = int(year) int_mon = int(mon) int_day = int(day) except ValueError: raise FormValueError, "year, month, and day must all be set" return self.datetime_validation(int_year, int_mon, int_day) def datetime_validation(self, year, mon, day): """datetime_validation(year : int, month : int, day : int) -> DateTime Create and return a DateTime instance based on the year, month, day arguments. Adjust the day to be within the legal range of days for that particular month/year combination. Account for a leap-year. """ typecheck(year, IntType) typecheck(mon, IntType) typecheck(day, IntType) adjusted_day = day if mon == 2: # feb # check for a leap-year if ((year % 4) == 0 and (year % 100) > 0) or (year % 400) == 0: if day > 29: adjusted_day = 29; elif day > 28: adjusted_day = 28; elif (mon == 4 or mon == 6 or mon == 9 or mon == 11) and day > 30: adjusted_day = 30; # Report an error if not auto_correcting and correction is needed if not self.auto_correct and adjusted_day <> day: raise FormValueError, ("The last day in %s, %d is %d" % (MONTH_NAMES[mon - 1], year, adjusted_day)) self.value = DateTime.DateTime(year, mon, adjusted_day) return self.value # Register this widget class from quixote.form import register_widget_class register_widget_class(DateSelectWidget)