Commit b151d6c1 by Don Mitchell

Merge pull request #183 from edx/dhm/timeconv/bug0

Fix a few remaining tz naive dates
parents a3c7ece8 a1f181e7
......@@ -53,6 +53,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
......
......@@ -3,6 +3,10 @@ from django.core.urlresolvers import reverse
from .utils import parse_json, user, registration
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
import datetime
from pytz import UTC
class ContentStoreTestCase(ModuleStoreTestCase):
......@@ -162,3 +166,21 @@ class AuthTestCase(ContentStoreTestCase):
self.assertEqual(resp.status_code, 302)
# Logged in should work.
class ForumTestCase(CourseTestCase):
def setUp(self):
""" Creates the test course. """
super(ForumTestCase, self).setUp()
self.course = CourseFactory.create(org='testX', number='727', display_name='Forum Course')
def test_blackouts(self):
now = datetime.datetime.now(UTC)
self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in
[(now - datetime.timedelta(days=14), now - datetime.timedelta(days=11)),
(now + datetime.timedelta(days=24), now + datetime.timedelta(days=30))]]
self.assertTrue(self.course.forum_posts_allowed)
self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in
[(now - datetime.timedelta(days=14), now + datetime.timedelta(days=2)),
(now + datetime.timedelta(days=24), now + datetime.timedelta(days=30))]]
self.assertFalse(self.course.forum_posts_allowed)
......@@ -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):
date_proxy = Date()
try:
blackout_periods = [(parse_time(start), parse_time(end))
blackout_periods = [(date_proxy.from_json(start),
date_proxy.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)
......
......@@ -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,28 @@ 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
PREVENT_DEFAULT_DAY_MON_SEED1 = datetime.datetime(CURRENT_YEAR, 1, 1, tzinfo=UTC)
PREVENT_DEFAULT_DAY_MON_SEED2 = datetime.datetime(CURRENT_YEAR, 2, 2, tzinfo=UTC)
def _parse_date_wo_default_month_day(self, field):
"""
Parse the field as an iso string but prevent dateutils from defaulting the day or month while
allowing it to default the other fields.
"""
# 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.PREVENT_DEFAULT_DAY_MON_SEED1)
result_other = dateutil.parser.parse(field, default=self.PREVENT_DEFAULT_DAY_MON_SEED1)
if result != result_other:
log.warning("Field {0} is missing month or day".format(self._name, field))
return None
if result.tzinfo is None:
result = result.replace(tzinfo=UTC)
return result
def from_json(self, field):
"""
Parse an optional metadata key containing a time: if present, complain
......@@ -26,14 +48,11 @@ class Date(ModelType):
elif field is "":
return None
elif isinstance(field, basestring):
result = dateutil.parser.parse(field)
if result.tzinfo is None:
result = result.replace(tzinfo=UTC())
return result
return self._parse_date_wo_default_month_day(field)
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:
......
......@@ -3,6 +3,8 @@ import datetime
import unittest
from django.utils.timezone import UTC
from xmodule.fields import Date, Timedelta
from xmodule.timeinfo import TimeInfo
import time
class DateTest(unittest.TestCase):
......@@ -52,6 +54,18 @@ class DateTest(unittest.TestCase):
self.assertEqual(
datetime.datetime(current.year, 12, 4, 16, 30, tzinfo=UTC()),
DateTest.date.from_json("December 4 16:30"))
self.assertIsNone(DateTest.date.from_json("12 12:00"))
def test_non_std_from_json(self):
"""
Test the non-standard args being passed to from_json
"""
now = datetime.datetime.now(UTC())
delta = now - datetime.datetime.fromtimestamp(0, UTC())
self.assertEqual(DateTest.date.from_json(delta.total_seconds() * 1000),
now)
yesterday = datetime.datetime.now(UTC()) - datetime.timedelta(days=-1)
self.assertEqual(DateTest.date.from_json(yesterday), yesterday)
def test_to_json(self):
'''
......@@ -90,3 +104,12 @@ class TimedeltaTest(unittest.TestCase):
'1 days 46799 seconds',
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
from xmodule.fields import Timedelta
log = logging.getLogger(__name__)
class TimeInfo(object):
......@@ -14,6 +13,7 @@ class TimeInfo(object):
self.close_date - the real due date
"""
_delta_standin = Timedelta()
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 = TimeInfo._delta_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))
......
"""
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