Merge lp:~jcsackett/launchpad/dumb-date-formats into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 12434
Proposed branch: lp:~jcsackett/launchpad/dumb-date-formats
Merge into: lp:launchpad
Diff against target: 129 lines (+85/-1)
2 files modified
lib/lp/app/widgets/date.py (+41/-1)
lib/lp/app/widgets/tests/test_datetime.py (+44/-0)
To merge this branch: bzr merge lp:~jcsackett/launchpad/dumb-date-formats
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+50385@code.launchpad.net

Commit message

[r=benji][bug=134063] Updates DateTime widget to kick back formats it can't possibly cope with.

Description of the change

Summary
=======

On the new sprint page, an OOPS can be triggered when dates that zope.date.parse can't understand get entered. Rather than throwing back an error, parse munges the dates into a format it can deal with, resulting in data that throws of db constraints.

This branch updates the widget to check if the date string is in any supported format, and kicks back an error message (rather than an OOPS) if it isn't.

Preimplementation
=================

Spoke with Curtis Hovey

Proposed Fix
============

Update the widget so it kicks back the input *before* the widget converts it into an erroneous datetime object.

Implementation
==============

The DateTimeWidget in lp.apps.widgets.date is provided with a new attribute, supported_input_formats, which is a list of format strings for datetime.strptime.

It also has a new method, _checkSupportedFormat, which uses the list of format strings to see if a meaningful datetime can be extracted from the input. If not, an error is raised.

Tests
=====

bin/test -m lp.app.widgets.tests

Demo & QA
=========

Try to enter the dates listed in the bug; they should come back with errors, rather than triggering an OOPS.

Lint
=====

make lint output:

= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
  lib/lp/app/widgets/date.py

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good. A few things to consider:

Deepening on the text of an exception message can be dangerous. Most
libraries don't consider them to be part of the API, so they may change
without warning. I would add a small test that ensures that the text
you expect ("unconverted data remains") is generated when you expect it
to be. That way if different text gets generated later we'll have a
test specifically about that fail instead of a hard to diagnose edge
case failure.

If that particular behavior had been in an upstream library and not the
Python stdlib, I would have suggested promoting a patch to the library
to raise two different exceptions so you wouldn't have to check the
message. However, given Python's (long) release cycle and the immanent
end of Python 2 releases, I wouldn't bother.

Also, trailing whitespace triggers the "unconverted data remains"
message, so I believe entering a date in an unsupported format with
trailing whitespace will cause the input to be considered valid and
cause an OOPS when it gets deeper into the app.

Someone other than myself might want the new test be done as a unittest
instead of a doctest. Since you're only adding a small test to an
already large set of doctests, I think it's fine as-is.

Lastly, all the accepted formats requires seconds. Do we also want to
accept date/times without seconds?

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/widgets/date.py'
2--- lib/lp/app/widgets/date.py 2011-02-01 21:01:02 +0000
3+++ lib/lp/app/widgets/date.py 2011-02-22 15:24:38 +0000
4@@ -109,6 +109,8 @@
5 """
6
7 timeformat = '%Y-%m-%d %H:%M:%S'
8+
9+
10 required_time_zone = None
11 display_zone = True
12 from_date = None
13@@ -121,7 +123,28 @@
14 def __init__(self, context, request):
15 request.needs_datetimepicker_iframe = True
16 super(DateTimeWidget, self).__init__(context, request)
17- self.system_time_zone = getUtility(ILaunchBag).time_zone
18+ launchbag = getUtility(ILaunchBag)
19+ self.system_time_zone = launchbag.time_zone
20+
21+ @property
22+ def supported_input_formats(self):
23+ date_formats = [
24+ '%Y-%m-%d', '%m-%d-%Y', '%m-%d-%y',
25+ '%m/%d/%Y', '%m/%d/%y',
26+ '%d %b, %Y', '%d %b %Y', '%b %d, %Y', '%b %d %Y',
27+ '%d %B, %Y', '%d %B %Y', '%B %d, %Y', 'B% %d %Y',
28+ ]
29+
30+ time_formats = [
31+ '%H:%M:%S',
32+ '%H:%M',
33+ '',
34+ ]
35+ outputs = []
36+ for fmt in time_formats:
37+ outputs.extend(['%s %s' % (d, fmt) for d in date_formats])
38+
39+ return [o.strip() for o in outputs]
40
41 #@property XXX: do as a property when we have python2.5 for tests of
42 #properties
43@@ -306,6 +329,21 @@
44 raise self._error
45 return value
46
47+ def _checkSupportedFormat(self, input):
48+ """Checks that the input is in a usable format."""
49+ for fmt in self.supported_input_formats:
50+ try:
51+ datetime.strptime(input.strip(), fmt)
52+ except (ValueError), e:
53+ if 'unconverted data remains' in e.message:
54+ return
55+ else:
56+ failure = e
57+ else:
58+ return
59+ if failure:
60+ raise ConversionError('Invalid date value', failure)
61+
62 def _toFieldValue(self, input):
63 """Return parsed input (datetime) as a date."""
64 return self._parseInput(input)
65@@ -346,6 +384,7 @@
66 """
67 if input == self._missing:
68 return self.context.missing_value
69+ self._checkSupportedFormat(input)
70 try:
71 year, month, day, hour, minute, second, dummy_tz = parse(input)
72 second, micro = divmod(second, 1.0)
73@@ -560,6 +599,7 @@
74
75 class DatetimeDisplayWidget(DisplayWidget):
76 """Display timestamps in the users preferred time zone"""
77+
78 def __call__(self):
79 time_zone = getUtility(ILaunchBag).time_zone
80 if self._renderedValueSet():
81
82=== added file 'lib/lp/app/widgets/tests/test_datetime.py'
83--- lib/lp/app/widgets/tests/test_datetime.py 1970-01-01 00:00:00 +0000
84+++ lib/lp/app/widgets/tests/test_datetime.py 2011-02-22 15:24:38 +0000
85@@ -0,0 +1,44 @@
86+from datetime import datetime
87+
88+from zope.app.form.interfaces import ConversionError
89+from zope.schema import Field
90+
91+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
92+from canonical.testing.layers import DatabaseFunctionalLayer
93+from lp.app.widgets.date import DateTimeWidget
94+from lp.testing import TestCase
95+
96+class TestDateTimeWidget(TestCase):
97+
98+ layer = DatabaseFunctionalLayer
99+
100+ def setUp(self):
101+ super(TestDateTimeWidget, self).setUp()
102+ field = Field(__name__='foo', title=u'Foo')
103+ request = LaunchpadTestRequest()
104+ self.widget = DateTimeWidget(field, request)
105+
106+ def test_unsupported_format_errors(self):
107+ # Dates in unsupported formats result in a ConversionError
108+ self.assertRaises(
109+ ConversionError,
110+ self.widget._checkSupportedFormat,
111+ '15-5-2010')
112+
113+ def test_unconverted_message(self):
114+ # The widget format checker relies on a particular mesage
115+ # being returned. If that breaks, this will tell us.
116+ test_str = "2010-01-01 10:10:10"
117+ fmt = "%Y-%m-%d"
118+ try:
119+ datetime.strptime(test_str, fmt)
120+ except (ValueError,), e:
121+ self.assertTrue('unconverted data' in e.message)
122+
123+ def test_whitespace_does_not_trick_validation(self):
124+ # Trailing whitespace doesn't get through because of the
125+ # unconverted data issue.
126+ self.assertRaises(
127+ ConversionError,
128+ self.widget._checkSupportedFormat,
129+ '15-5-2010 ')