From 48ee275799bf8266620f12b5a8cb0825e6a38438 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Mon, 17 Jun 2013 09:37:43 -0400 Subject: [PATCH] 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) --- CHANGELOG.rst | 3 ++ common/lib/xmodule/xmodule/course_module.py | 16 ++++--- common/lib/xmodule/xmodule/fields.py | 21 +++++++--- common/lib/xmodule/xmodule/timeinfo.py | 6 +-- common/lib/xmodule/xmodule/timeparse.py | 46 --------------------- 5 files changed, 32 insertions(+), 60 deletions(-) delete mode 100644 common/lib/xmodule/xmodule/timeparse.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bbbb023527..d78d1c4cb6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 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: Safety code added to prevent anything above the vertical level in the diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index d0333cbe36..5dff0e1a20 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -10,7 +10,6 @@ import dateutil.parser from xmodule.modulestore import Location from xmodule.seq_module import SequenceDescriptor, SequenceModule -from xmodule.timeparse import parse_time from xmodule.util.decorators import lazyproperty from xmodule.graders import grader_from_conf import json @@ -645,8 +644,11 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): def start_date_text(self): def try_parse_iso_8601(text): try: - result = datetime.strptime(text, "%Y-%m-%dT%H:%M") - result = result.strftime("%b %d, %Y") + result = Date().from_json(text) + if result is None: + result = text.title() + else: + result = result.strftime("%b %d, %Y") except ValueError: result = text.title() @@ -670,8 +672,10 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): @property def forum_posts_allowed(self): + datestandin = Date() try: - blackout_periods = [(parse_time(start), parse_time(end)) + blackout_periods = [(datestandin.from_json(start), + datestandin.from_json(end)) for start, end in self.discussion_blackouts] now = datetime.now(UTC()) @@ -701,7 +705,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): if self.last_eligible_appointment_date is None: raise ValueError("Last appointment date must be specified") 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 # do validation within the exam info: if self.registration_start_date > self.registration_end_date: @@ -720,7 +724,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): """ if key in self.exam_info: try: - return parse_time(self.exam_info[key]) + return Date().from_json(self.exam_info[key]) 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) log.warning(msg) diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py index 963b70204e..6dcab120e4 100644 --- a/common/lib/xmodule/xmodule/fields.py +++ b/common/lib/xmodule/xmodule/fields.py @@ -6,7 +6,7 @@ from xblock.core import ModelType import datetime import dateutil.parser -from django.utils.timezone import UTC +from pytz import UTC log = logging.getLogger(__name__) @@ -15,6 +15,10 @@ class Date(ModelType): ''' 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): """ Parse an optional metadata key containing a time: if present, complain @@ -26,14 +30,21 @@ class Date(ModelType): elif field is "": return None 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 year, month, or day".format(self._name, field)) + return None if result.tzinfo is None: - result = result.replace(tzinfo=UTC()) + result = result.replace(tzinfo=UTC) return result 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): - return datetime.datetime.fromtimestamp(time.mktime(field), UTC()) + return datetime.datetime.fromtimestamp(time.mktime(field), UTC) elif isinstance(field, datetime.datetime): return field else: diff --git a/common/lib/xmodule/xmodule/timeinfo.py b/common/lib/xmodule/xmodule/timeinfo.py index 9a63c0477d..bd29c938e3 100644 --- a/common/lib/xmodule/xmodule/timeinfo.py +++ b/common/lib/xmodule/xmodule/timeinfo.py @@ -1,6 +1,5 @@ -from .timeparse import parse_timedelta - import logging +from xmodule.fields import Date log = logging.getLogger(__name__) class TimeInfo(object): @@ -14,6 +13,7 @@ class TimeInfo(object): self.close_date - the real due date """ + _date_standin = Date() def __init__(self, due_date, grace_period_string): if due_date is not None: self.display_due_date = due_date @@ -23,7 +23,7 @@ class TimeInfo(object): if grace_period_string is not None and self.display_due_date: try: - self.grace_period = parse_timedelta(grace_period_string) + self.grace_period = self._date_standin.from_json(grace_period_string) self.close_date = self.display_due_date + self.grace_period except: log.error("Error parsing the grace period {0}".format(grace_period_string)) diff --git a/common/lib/xmodule/xmodule/timeparse.py b/common/lib/xmodule/xmodule/timeparse.py deleted file mode 100644 index b189262761..0000000000 --- a/common/lib/xmodule/xmodule/timeparse.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -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\d+?) day(?:s?))?(\s)?((?P\d+?) hour(?:s?))?(\s)?((?P\d+?) minute(?:s)?)?(\s)?((?P\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: - day[s] (optional) - hour[s] (optional) - minute[s] (optional) - 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)