"""
open/dulcinea/lib/ui/form/date_time_widget.qpy
"""
from datetime import datetime, timedelta
from dulcinea.ui.form.widget import PairWidget
from qp.fill.form import Form, add_javascript_code
from qp.fill.html import htmltag
from qp.fill.widget import IntWidget, SingleSelectWidget, Widget
from qp.fill.widget import OptionSelectWidget, CompositeWidget
from qp.fill.widget import WidgetValueError, CompositeWidget
from qp.lib.spec import require, boolean, datetime_with_tz
from qp.pub.common import get_request, get_response, site_now, get_publisher
from qp.pub.common import get_fields
import sys
class MinuteWidget (IntWidget):
TYPE_ERROR = 'minute must be a number between 0 and 59'
def _parse(self, request):
IntWidget._parse(self, request)
if self.value is None:
self.error = None
self.value = 0
if not self.error and not 0 <= self.value <= 59:
self.error = self.TYPE_ERROR
def render_content(self):
try:
rendered_value = "%02d" % (self.value or 0)
except TypeError:
rendered_value = str(self.value)
return htmltag("input", xml_end=True,
type=self.HTML_TYPE,
name=self.name,
value=rendered_value,
**self.attrs)
def render(self):
return self.render_content()
class HourWidget (IntWidget):
TYPE_ERROR = 'hour must be a number between 0 and 23'
def _parse(self, request):
IntWidget._parse(self, request)
if not self.error and not 0 <= self.value <= 23:
self.error = self.TYPE_ERROR
class DateTimeWidget (CompositeWidget):
year_offsets = (-1, 5)
months = [(1, "January"),
(2, "February"),
(3, 'March'),
(4, 'April'),
(5, 'May'),
(6, 'June'),
(7, 'July'),
(8, 'August'),
(9, 'September'),
(10, 'October'),
(11, 'November'),
(12, 'December' )]
def __init__(self, name, value=None, **kwargs):
if value is None:
value = site_now()
CompositeWidget.__init__(self, name, value, **kwargs)
self.add(IntWidget, 'year', value=value.year, size=4, maxlength=4)
self.add(SingleSelectWidget, 'month', value=value.month,
options=self.months, sort=False)
self.add(IntWidget, 'day', value=value.day, size=2, maxlength=2)
self.add(HourWidget, 'hour', value=value.hour, size=2, maxlength=2)
self.add(MinuteWidget, 'minute', value=value.minute,
size=2, maxlength=2)
def _parse(self, request):
self.value = None
if request.get_field(self.name):
try:
# The value is being provided directly (from a query string)
# and not from the subwidgets.
value = datetime.fromtimestamp(
int(request.get_field(self.name)))
value.replace(tzinfo=get_publisher().get_time_zone())
except (ValueError, TypeError):
self.set_error("bad date value")
else:
for widget in self.get_widgets():
widget.clear_error(request)
self.set_value(value)
return
year = self.get('year')
month = self.get('month')
day = self.get('day')
hour = self.get('hour')
minute = self.get('minute') or 0
if None in (year, month, day, hour):
self.set_error("incomplete date")
return
this_year = site_now().year
min_year = this_year + self.year_offsets[0]
max_year = this_year + self.year_offsets[1]
if not min_year <= year <= max_year:
self.set_error('year must be between %s and %s' % (
min_year, max_year))
return
try:
self.value = datetime(year, month, day, hour, minute,
tzinfo=get_publisher().get_time_zone())
except (ValueError, TypeError):
err = sys.exc_info()[1]
self.set_error(str(err))
def set_value(self, value):
self.value = value
if value is None:
for widget in self.get_widgets():
widget.set_value(None)
else:
self.get_widget('year').set_value(value.year)
self.get_widget('month').set_value(value.month)
self.get_widget('day').set_value(value.day)
self.get_widget('hour').set_value(value.hour)
self.get_widget('minute').set_value(value.minute)
def render_content:xml(self):
self.get_widget('day').render_content()
self.get_widget('month').render_content()
self.get_widget('year').render_content()
' at '
self.get_widget('hour').render_content()
' : '
self.get_widget('minute').render_content()
class DateTimeSelectWidget (Widget):
"""\
DateTimeSelectWidget
The UI components of DateTimeSelectWidget SingleSelectWidgets
arranged in a compound widget fashion and as a result tends to
be more friendly to "clickers" rather than "typers".
DateTimeSelectWidget has a fair bit of available magic
that can minimize the amount of clicks needed to enter a date and time.
All the magical bits are off by default.
'value' is either a Python datetime instance or None.
The 'show_none' boolean keyword argument is on by default. With
'show_none' enabled, each of the SingleSelect subwidgets will have
a blank choice. If the 'value' is None, the widget will render itself
with all blanks. When the widget is parsed with each subwidget in
this None postion, the value of the parsed widget is None. If the
'show_none' is disabled (meaning there is no way for the user to make
the value of the widget None) and the 'value' passed in is None,
the widget will render itself with a date corresponding to now as
the default.
DateTimeSelectWidget also takes a boolean 'JS_fix' keyword argument
that is off 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_fix' boolean keyword
is True, the same behaviour that 'JS_fix' implements is performed
on the server, that is to automatically correct invalid dates.
If 'auto_fix' is False, a WidgetValueError is raised if the
widget contains an illegal date. The exception contains the
information that would be used for the correction. The latter is the
default behavior.
DateTimeSelectWidget takes a 'show_hour_min' boolean keyword argument
that is False by default. When enabled, additional HTML will be
generated to display the Hour and Minute (in 15 minute increments)
AM or PM. This information will be used to apply more detail
to the datetime object that's displayed and returned.
DateTimeSelectWidget takes a 'JS_time' argument which is None by
default. If this argument is non-None, it is expected to be an
int value that is interpreted as the number of minutes to add or
subtract from the time that the *client* thinks it is via a Javascript
query. If we're able to get a date & time from the client, then
this time (with the hours adjustment applied) overrides what is
displayed for the time when the widget is rendered. If there's
an error obtaining the date & time via Javascript, then the
JS_time argument is ignored.
DateTimeSelectWidget takes optional 'start_year' and 'end_year' keyword
arguments. When specified, the years that are displayed as selectable
items will be years between 'start_year' and 'end_year' inclusive.
The default is to display 3 years ahead and 3 years behind the year
corresponding to the widget's initial value.
"""
MONTH_NAMES = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
def __init__(self, name, value=None, JS_fix=False, JS_time=None,
auto_fix=False, show_hour_min=False, show_none=True,
start_year=None, end_year=None, **kwattrs):
require(value, (datetime_with_tz, None))
require(JS_time, (int, None))
require(auto_fix, boolean)
require(show_hour_min, boolean)
require(show_none, boolean)
require(start_year, (int, None))
require(end_year, (int, None))
self.auto_fix = auto_fix # correct the date like 'JS_fix' does
# otherwise raise WidgetValueError
self.JS_fix = JS_fix # insert Javascript for on-the-fly
# client-side date/time validation
self.JS_time = JS_time # use client's version of date/time
# adjusted by the value of this arg
self.show_none = show_none # show empty selection in options
self.show_hour_min = show_hour_min
self.start_year = start_year
self.end_year = end_year
Widget.__init__(self, self.safe_name(name), value=value, **kwattrs)
self.year_name = self.safe_name(name + "_year")
self.month_name = self.safe_name(name + "_month")
self.day_name = self.safe_name(name + "_day")
if show_hour_min:
self.hour_name = self.safe_name(name + "_hour")
self.min_name = self.safe_name(name + "_min")
self.am_pm_name = self.safe_name(name + "_am_pm")
def safe_name(self, name):
"""return a name that is safe to use with subwidgets (no '.')
"""
return "_".join(name.split('.'))
def render(self):
if self.JS_fix:
self._add_JS_fix()
if self.JS_time is not None:
self._add_JS_time()
self._add_JS_time_call()
return Widget.render(self)
def is_submitted(self, request=None):
if request is None:
request = get_request()
fields = get_fields()
return (self.year_name in fields and
self.month_name in fields and
self.day_name in fields)
def _parse(self, request):
get_field = request.get_field
year_str = get_field(self.year_name)
mon_str = get_field(self.month_name)
day_str = get_field(self.day_name)
if self.show_hour_min:
hour_str = get_field(self.hour_name)
min_str = get_field(self.min_name)
am_pm = get_field(self.am_pm_name)
# look for the 'not-selected' option
if not year_str and not mon_str and not day_str:
self.value = None
return self.value
# user has tried to select a date
try:
year = int(year_str)
mon = int(mon_str)
day = int(day_str)
except ValueError:
raise WidgetValueError("year, month, and day must all be set")
if self.show_hour_min:
# capture potential specification of hours and minutes
try:
hour = int(hour_str)
except (ValueError, TypeError):
hour = 0
try:
min = int(min_str)
except (ValueError, TypeError):
min = 0
if am_pm == 'PM' and hour != 12:
hour += 12
else:
hour = min = 0
self.value = self._new_datetime(
year, mon, day, hour, min, get_publisher().get_time_zone())
return self.value
def _new_datetime(self, year, mon, day, hour, min, tzinfo):
"""(year : int, month : int, day : int, hour : int, min : 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.
"""
require(year, int)
require(mon, int)
require(day, int)
require(hour, int)
require(min, int)
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_fix and adjusted_day != day:
raise WidgetValueError("The last day in %s, %d is %d"
% (self.MONTH_NAMES[mon - 1],
year, adjusted_day))
return datetime(year, mon, adjusted_day, hour, min, tzinfo=tzinfo)
def render_content:xml(self):
value = self.value
now = site_now()
if value is None:
selected = ' selected="selected"'
else:
selected = ""
if self.show_none:
not_selected_option = '' % selected
else:
not_selected_option = ''
if value is None:
value = now
if self.JS_fix:
if self.show_hour_min:
script_ref = ('onchange="date_select_fix_day(this.form.%s,'
'this.form.%s,this.form.%s,this.form.%s,'
'this.form.%s,this.form.%s,%s)"') % (
self.year_name, self.month_name, self.day_name,
self.hour_name, self.min_name, self.am_pm_name,
int(bool(not_selected_option)))
else:
script_ref = ('onchange="date_select_fix_day(this.form.%s,'
'this.form.%s,this.form.%s,null,null,null,%s)"') % (
self.year_name, self.month_name, self.day_name,
int(bool(not_selected_option)))
else:
script_ref = ''
# year select
"""
"""
def date_in_month(date, month):
try:
return date and date.strftime("%b") == month
except ValueError:
exc = sys.exc_info()[1]
self.set_error(exc)
# month select
"""
"""
# day select
"""
"""
if self.show_hour_min:
# hour:min select
"""- :
"""
# AM/PM select
"""
"""
''
def _add_JS_fix(self):
# Add a javascript function that mimicks the auto_fix behavior of
# the self._new_datetime method on the client side
add_javascript_code('_date_select_fix', """\
function date_select_fix_day(ys, ms, ds, hs, mins, ampms, size) {
var y = parseInt(ys.options[ys.selectedIndex].value)
var m = parseInt(ms.options[ms.selectedIndex].value)
var d = parseInt(ds.options[ds.selectedIndex].value)
/* if year is unselected, unselect everything */
if (isNaN(y)) {
ms.selectedIndex = ds.selectedIndex = 0
if (hs && mins && ampms) {
hs.selectedIndex = 0
mins.selectedIndex = 0
ampms.selectedIndex = 0
}
}
else if (m == 2) {
if (((y % 4) == 0) &&
(((y % 100) > 0) || ((y % 400) == 0))) {
if (d > 29)
ds.selectedIndex = size + 28;
} else if (d > 28)
ds.selectedIndex = size + 27;
} else if ((m == 4 || m == 6 || m == 9 || m == 11) && d > 30)
ds.selectedIndex = size + 29;
}
""")
def _add_JS_time(self):
# Add a javascript function that allows the javascript Date
# object (modifyable by a minutes delta) to be used to set the time
add_javascript_code('_date_select_time', """
function set_selected (form, widget, selected_value) {
for (i = 0; i < form.elements.length; i++)
if (form.elements[i] == widget)
for (j = 0; j < widget.options.length; j++)
if (widget.options[j].value == selected_value)
widget.options[j].selected = true;
}
function date_select_set_time (minutes_delta, form, year_select,
month_select, day_select,
hour_select, min_select,
am_pm_select) {
var d1 = new Date();
var date = new Date(d1.getTime() + (minutes_delta * 60000));
var ampm = "AM";
var hours = date.getHours();
var minutes = date.getMinutes();
if (hours == 0) hours = 12;
if (hours > 11) ampm = "PM";
if (hours > 12) hours -= 12;
if (0 < minutes && minutes < 15) minutes = 0;
else if (15 < minutes && minutes < 30) minutes = 15;
else if (30 < minutes && minutes < 45) minutes = 30;
else if (45 < minutes && minutes <= 59) minutes = 45;
set_selected(form, year_select, date.getFullYear());
set_selected(form, month_select, date.getMonth()+1);
set_selected(form, day_select, date.getDate());
set_selected(form, hour_select, hours);
set_selected(form, min_select, minutes);
set_selected(form, am_pm_select, ampm);
}
""")
def _add_JS_time_call(self):
add_javascript_code(self.name, """
date_select_set_time(%d, document.forms[0],
document.forms[0].%s,
document.forms[0].%s,
document.forms[0].%s,
document.forms[0].%s,
document.forms[0].%s,
document.forms[0].%s)
""" % (self.JS_time, self.year_name,
self.month_name, self.day_name,
self.hour_name, self.min_name,
self.am_pm_name))
class DatePairWidget(PairWidget):
def __init__(self, name, value=None, **kwargs):
PairWidget.__init__(self, name, value,
element_type=DateTimeSelectWidget,
element1_kwargs=dict(title='Start Date',
show_none=False),
element2_kwargs=dict(title='End Date',
show_none=False),
**kwargs)
def _parse(self, request):
PairWidget._parse(self, request)
if self.value:
self.value = (self.value[0].replace(hour=0, minute=0),
self.value[1].replace(hour=23, minute=59))
def get_date_pair_form(csv=True, start_date=None, end_date=None):
form = Form(use_tokens=False)
if end_date is None:
end_date = site_now()
if start_date is None:
start_date = end_date - timedelta(30)
get_response().set_expires(minutes=10)
form.add(DatePairWidget, 'date_pair', value=(start_date, end_date))
form.add_submit('search', 'Search')
if csv:
form.add_submit(
'csv', 'Spreadsheet',
attrs=dict(title='Download as a spread sheet file (CSV format)'))
return form
def get_period_select_form():
form = Form()
now = site_now()
dates = [(now - timedelta(30), "Past 30 days"),
(now - timedelta(60), "Past 60 days"),
(now - timedelta(90), "Past 90 days"),
(now - timedelta(182), "Past 6 months"),
(now - timedelta(365), "Past year")]
form.add(OptionSelectWidget, "date", options=dates, title='Timeframe')
return form