Commit cb40e9ff by Don Mitchell

Fix a few remaining tz naive dates

And remove redundant but != date parsing methods. In process make
the general parsing function less lenient (don't default date nor
month)
parent 1ef29053
...@@ -34,6 +34,9 @@ Blades: Staff debug info is now accessible for Graphical Slider Tool problems. ...@@ -34,6 +34,9 @@ Blades: Staff debug info is now accessible for Graphical Slider Tool problems.
Blades: For Video Alpha the events ready, play, pause, seek, and speed change Blades: For Video Alpha the events ready, play, pause, seek, and speed change
are logged on the server (in the logs). are logged on the server (in the logs).
Common: all dates and times are not time zone aware datetimes. No code should create or use struct_times nor naive
datetimes.
Common: Developers can now have private Django settings files. Common: Developers can now have private Django settings files.
Common: Safety code added to prevent anything above the vertical level in the Common: Safety code added to prevent anything above the vertical level in the
......
...@@ -10,7 +10,6 @@ import dateutil.parser ...@@ -10,7 +10,6 @@ import dateutil.parser
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time
from xmodule.util.decorators import lazyproperty from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf from xmodule.graders import grader_from_conf
import json import json
...@@ -645,8 +644,11 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -645,8 +644,11 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
def start_date_text(self): def start_date_text(self):
def try_parse_iso_8601(text): def try_parse_iso_8601(text):
try: try:
result = datetime.strptime(text, "%Y-%m-%dT%H:%M") result = Date().from_json(text)
result = result.strftime("%b %d, %Y") if result is None:
result = text.title()
else:
result = result.strftime("%b %d, %Y")
except ValueError: except ValueError:
result = text.title() result = text.title()
...@@ -670,8 +672,10 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -670,8 +672,10 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
@property @property
def forum_posts_allowed(self): def forum_posts_allowed(self):
datestandin = Date()
try: try:
blackout_periods = [(parse_time(start), parse_time(end)) blackout_periods = [(datestandin.from_json(start),
datestandin.from_json(end))
for start, end for start, end
in self.discussion_blackouts] in self.discussion_blackouts]
now = datetime.now(UTC()) now = datetime.now(UTC())
...@@ -701,7 +705,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -701,7 +705,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
if self.last_eligible_appointment_date is None: if self.last_eligible_appointment_date is None:
raise ValueError("Last appointment date must be specified") raise ValueError("Last appointment date must be specified")
self.registration_start_date = (self._try_parse_time('Registration_Start_Date') or self.registration_start_date = (self._try_parse_time('Registration_Start_Date') or
datetime.utcfromtimestamp(0)) datetime.fromtimestamp(0, UTC()))
self.registration_end_date = self._try_parse_time('Registration_End_Date') or self.last_eligible_appointment_date self.registration_end_date = self._try_parse_time('Registration_End_Date') or self.last_eligible_appointment_date
# do validation within the exam info: # do validation within the exam info:
if self.registration_start_date > self.registration_end_date: if self.registration_start_date > self.registration_end_date:
...@@ -720,7 +724,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -720,7 +724,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
""" """
if key in self.exam_info: if key in self.exam_info:
try: try:
return parse_time(self.exam_info[key]) return Date().from_json(self.exam_info[key])
except ValueError as e: except ValueError as e:
msg = "Exam {0} in course {1} loaded with a bad exam_info key '{2}': '{3}'".format(self.exam_name, self.course_id, self.exam_info[key], e) msg = "Exam {0} in course {1} loaded with a bad exam_info key '{2}': '{3}'".format(self.exam_name, self.course_id, self.exam_info[key], e)
log.warning(msg) log.warning(msg)
......
...@@ -6,7 +6,7 @@ from xblock.core import ModelType ...@@ -6,7 +6,7 @@ from xblock.core import ModelType
import datetime import datetime
import dateutil.parser import dateutil.parser
from django.utils.timezone import UTC from pytz import UTC
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -15,6 +15,10 @@ class Date(ModelType): ...@@ -15,6 +15,10 @@ class Date(ModelType):
''' '''
Date fields know how to parse and produce json (iso) compatible formats. Converts to tz aware datetimes. Date fields know how to parse and produce json (iso) compatible formats. Converts to tz aware datetimes.
''' '''
# See note below about not defaulting these
CURRENT_YEAR = datetime.datetime.now(UTC).year
DEFAULT_DATE0 = datetime.datetime(CURRENT_YEAR, 1, 1, tzinfo=UTC)
DEFAULT_DATE1 = datetime.datetime(CURRENT_YEAR, 2, 2, tzinfo=UTC)
def from_json(self, field): def from_json(self, field):
""" """
Parse an optional metadata key containing a time: if present, complain Parse an optional metadata key containing a time: if present, complain
...@@ -26,14 +30,21 @@ class Date(ModelType): ...@@ -26,14 +30,21 @@ class Date(ModelType):
elif field is "": elif field is "":
return None return None
elif isinstance(field, basestring): elif isinstance(field, basestring):
result = dateutil.parser.parse(field) # It's not trivial to replace dateutil b/c parsing timezones as Z, +03:30, -400 is hard in python
# however, we don't want dateutil to default the month or day (but some tests at least expect
# us to default year); so, we'll see if dateutil uses the defaults for these the hard way
result = dateutil.parser.parse(field, default=self.DEFAULT_DATE0)
result_other = dateutil.parser.parse(field, default=self.DEFAULT_DATE1)
if result != result_other:
log.warning("Field {0} is missing month or day".format(self._name, field))
return None
if result.tzinfo is None: if result.tzinfo is None:
result = result.replace(tzinfo=UTC()) result = result.replace(tzinfo=UTC)
return result return result
elif isinstance(field, (int, long, float)): elif isinstance(field, (int, long, float)):
return datetime.datetime.fromtimestamp(field / 1000, UTC()) return datetime.datetime.fromtimestamp(field / 1000, UTC)
elif isinstance(field, time.struct_time): elif isinstance(field, time.struct_time):
return datetime.datetime.fromtimestamp(time.mktime(field), UTC()) return datetime.datetime.fromtimestamp(time.mktime(field), UTC)
elif isinstance(field, datetime.datetime): elif isinstance(field, datetime.datetime):
return field return field
else: else:
......
...@@ -3,6 +3,7 @@ import datetime ...@@ -3,6 +3,7 @@ import datetime
import unittest import unittest
from django.utils.timezone import UTC from django.utils.timezone import UTC
from xmodule.fields import Date, Timedelta from xmodule.fields import Date, Timedelta
from xmodule.timeinfo import TimeInfo
class DateTest(unittest.TestCase): class DateTest(unittest.TestCase):
...@@ -52,6 +53,7 @@ class DateTest(unittest.TestCase): ...@@ -52,6 +53,7 @@ class DateTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
datetime.datetime(current.year, 12, 4, 16, 30, tzinfo=UTC()), datetime.datetime(current.year, 12, 4, 16, 30, tzinfo=UTC()),
DateTest.date.from_json("December 4 16:30")) DateTest.date.from_json("December 4 16:30"))
self.assertIsNone(DateTest.date.from_json("12 12:00"))
def test_to_json(self): def test_to_json(self):
''' '''
...@@ -90,3 +92,12 @@ class TimedeltaTest(unittest.TestCase): ...@@ -90,3 +92,12 @@ class TimedeltaTest(unittest.TestCase):
'1 days 46799 seconds', '1 days 46799 seconds',
TimedeltaTest.delta.to_json(datetime.timedelta(days=1, hours=12, minutes=59, seconds=59)) TimedeltaTest.delta.to_json(datetime.timedelta(days=1, hours=12, minutes=59, seconds=59))
) )
class TimeInfoTest(unittest.TestCase):
def test_time_info(self):
due_date = datetime.datetime(2000, 4, 14, 10, tzinfo=UTC())
grace_pd_string = '1 day 12 hours 59 minutes 59 seconds'
timeinfo = TimeInfo(due_date, grace_pd_string)
self.assertEqual(timeinfo.close_date,
due_date + Timedelta().from_json(grace_pd_string))
from .timeparse import parse_timedelta
import logging import logging
from xmodule.fields import Timedelta
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class TimeInfo(object): class TimeInfo(object):
...@@ -14,6 +13,7 @@ class TimeInfo(object): ...@@ -14,6 +13,7 @@ class TimeInfo(object):
self.close_date - the real due date self.close_date - the real due date
""" """
_delta_standin = Timedelta()
def __init__(self, due_date, grace_period_string): def __init__(self, due_date, grace_period_string):
if due_date is not None: if due_date is not None:
self.display_due_date = due_date self.display_due_date = due_date
...@@ -23,7 +23,7 @@ class TimeInfo(object): ...@@ -23,7 +23,7 @@ class TimeInfo(object):
if grace_period_string is not None and self.display_due_date: if grace_period_string is not None and self.display_due_date:
try: try:
self.grace_period = parse_timedelta(grace_period_string) self.grace_period = TimeInfo._delta_standin.from_json(grace_period_string)
self.close_date = self.display_due_date + self.grace_period self.close_date = self.display_due_date + self.grace_period
except: except:
log.error("Error parsing the grace period {0}".format(grace_period_string)) log.error("Error parsing the grace period {0}".format(grace_period_string))
......
"""
Helper functions for handling time in the format we like.
"""
import re
from datetime import timedelta, datetime
TIME_FORMAT = "%Y-%m-%dT%H:%M"
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
def parse_time(time_str):
"""
Takes a time string in TIME_FORMAT
Returns it as a time_struct.
Raises ValueError if the string is not in the right format.
"""
return datetime.strptime(time_str, TIME_FORMAT)
def stringify_time(dt):
"""
Convert a datetime struct to a string
"""
return dt.isoformat()
def parse_timedelta(time_str):
"""
time_str: A string with the following components:
<D> day[s] (optional)
<H> hour[s] (optional)
<M> minute[s] (optional)
<S> second[s] (optional)
Returns a datetime.timedelta parsed from the string
"""
parts = TIMEDELTA_REGEX.match(time_str)
if not parts:
return
parts = parts.groupdict()
time_params = {}
for (name, param) in parts.iteritems():
if param:
time_params[name] = int(param)
return timedelta(**time_params)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment