# Author: Michael DiBernardo (mikedebo@gmail.com)fromdjangoimportnewformsasformsfromdjango.newformsimportValidationErrorimportdatetimeimportreclassKungfuTimeField(forms.Field):""" Extension to Django's time fields that parses a much larger range of times without explicitly needing to specify all the time formats yourself. """# Matches any string with a 24-hourish format (sans AM/PM) but puts no# limits on the size of the numbers (e.g. 64:99 is OK.)_24_HOUR_PATTERN_STRING=r'^\s*(?P<hour>\d\d?)\s*:?\s*(?P<minute>\d\d)?\s*'# Matches any string with a 12-hourish format (with AM/PM) but puts no# limits on the size of the numbers (e.g. 64:99pm is OK.)_TIME_PATTERN_STRING=_24_HOUR_PATTERN_STRING+ \
r'((?P<ampm>[AaPp])\s*\.?\s*[Mm]?\s*\.?\s*)?$'# Matcher for our time pattern._TIME_PATTERN=re.compile(_TIME_PATTERN_STRING)# Validation error messages._ERROR_MESSAGES={'invalid':u'Enter a valid time.',}def__init__(self,*args,**kwargs):""" Create a new KungFuTimeField with the mojo of a thousand TimeFields. """super(KungfuTimeField,self).__init__(*args,**kwargs)defclean(self,value):""" Parses datetime from the given value. If it can't figure it out, throws a ValidationError. """super(KungfuTimeField,self).clean(value)ifnotvalue:returnNoneifisinstance(value,datetime.time):returnvaluecleaned=self._parse_time(value)returncleaneddef_parse_time(self,value):""" Tries to recognize a time using our hefty regexp. If it doesn't match the regexp, throws a validation error. If it DOES match but the resulting numbers are out of range (e.g. an hour of 99), will also throw a ValidationError. """match=self._TIME_PATTERN.match(value)ifnotmatch:raiseValidationError(self._ERROR_MESSAGES['invalid'])# Hour has to be there because it's required in the regexp. Set the# minute to 0 for now. We dunno if there's AM/PM or not.(hour,minute,ampm)=(int(match.group('hour')),0,match.group('ampm'))# Let's see if the user typed a minute.try:# Raises TypeError if group fetch returns None.minute=int(match.group('minute'))exceptTypeError:passifampm:hour=self._handle_twelve_hour_time(hour,minute,ampm)# If the numbers are out of range, we'll find out here.try:returndatetime.time(hour,minute)exceptValueError:raiseValidationError(self._ERROR_MESSAGES['invalid'])# I don't expect any other problems, but if there are, they'll# propagate.def_handle_twelve_hour_time(self,hour,minute,ampm):""" Detect 24 hour time with an am/pm (e.g. 18:30pm, 0:30am) and do the necessary transform for converting pm times to 24 hour times. """ifhour<1orhour>12:raiseValidationError(self._ERROR_MESSAGES['invalid'])elifampm.lower()=="a"andhour==12:return0elifampm.lower()=="p"and1<=hourandhour<=11:returnhour+12else:returnhour"""Tests that our extension to the Django time field can parse a wide variety oftime formats."""# Author: Michael DiBernardo (mikedebo@gmail.com)fromformhelpers.fieldsimportKungfuTimeFieldfromdjango.newformsimportValidationErrorimportdatetimeasdtimportunittestclassTestTimeParsing(unittest.TestCase):defsetUp(self):self.field=KungfuTimeField()deftest24HourFormats(self):""" Tests a variety of 24 hour formats. """twofour_hour_tests=(("8:30",dt.time(8,30)),("14:30",dt.time(14,30)),("8",dt.time(8)),("16",dt.time(16)),("1742",dt.time(17,42)),("856",dt.time(8,56)),("101",dt.time(1,01)),("1 01",dt.time(1,01)),("13 01",dt.time(13,01)),("01 01",dt.time(1,1)),(" 13 01 ",dt.time(13,01)),)for(input,expected)intwofour_hour_tests:self.assertTimeEquals(input,expected)deftest12HourFormats(self):""" Tests a variety of 12 hour formats. """twelve_hour_tests=(("12:30pm",dt.time(12,30)),("12:30PM",dt.time(12,30)),("12:30P.m.",dt.time(12,30)),("12:30 pm ",dt.time(12,30)),("12:30 P m",dt.time(12,30)),("12:30 p . M .",dt.time(12,30)),("12:30p",dt.time(12,30)),("12:30 p",dt.time(12,30)),("12 30pm",dt.time(12,30)),("12 30 pm",dt.time(12,30)),("1230 p m",dt.time(12,30)),(" 1230 p m",dt.time(12,30)),("12 30 p . m .",dt.time(12,30)),("1:30am",dt.time(1,30)),("1:30 am ",dt.time(1,30)),("1:30 a m",dt.time(1,30)),("1:30 a . m .",dt.time(1,30)),("1:30a",dt.time(1,30)),("1:30 a",dt.time(1,30)),("1 30am",dt.time(1,30)),(" 1 30 am",dt.time(1,30)),("130 a m",dt.time(1,30)),("3:30 p m",dt.time(15,30)),(" 1 30 a . m ",dt.time(1,30)),)for(input,expected)intwelve_hour_tests:self.assertTimeEquals(input,expected)deftestBadTimes(self):""" Tests a variety of times that should bork and die, not necessarily in that order. """bad_inputs=(""," aa ","12345","1340am","030pm","25:23","24:10","this ain't nothing like a time","-2:30",)forbad_inputinbad_inputs:try:self.field.clean(bad_input)self.fail("Bad input %s validated."%bad_input)exceptAssertionError,e:raiseeexceptValidationError,e:passexceptException,e:self.fail("Validator threw unexpected exception %s"%str(e))defassertTimeEquals(self,timestring,expected):""" Try to parse a time, and if it throws an exception, fail. """try:actual=self.field.clean(timestring)self.assertEquals(actual,expected,"String %s did not parse to expected datetime %s: Got %s"%(timestring,str(expected),str(actual)))exceptAssertionError,e:# Propagate assertion failures.raiseeexceptValidationError,e:self.fail("String %s did not validate."%timestring)exceptException,e:self.fail("String %s caused unexpected exception: %s"%(timestring,str(e)))