Commit 8370124e by Don Mitchell

Make to and from json for dates use iso format esp for timezone.

parent 25ceea17
...@@ -28,19 +28,35 @@ from xmodule.modulestore.django import modulestore ...@@ -28,19 +28,35 @@ from xmodule.modulestore.django import modulestore
class ConvertersTestCase(TestCase): class ConvertersTestCase(TestCase):
@staticmethod @staticmethod
def struct_to_datetime(struct_time): def struct_to_datetime(struct_time):
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, struct_time.tm_mday, struct_time.tm_hour, return datetime.datetime(struct_time.tm_year, struct_time.tm_mon,
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC()) struct_time.tm_mday, struct_time.tm_hour,
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
def compare_dates(self, date1, date2, expected_delta): def compare_dates(self, date1, date2, expected_delta):
dt1 = ConvertersTestCase.struct_to_datetime(date1) dt1 = ConvertersTestCase.struct_to_datetime(date1)
dt2 = ConvertersTestCase.struct_to_datetime(date2) dt2 = ConvertersTestCase.struct_to_datetime(date2)
self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" + str(date2) + "!=" + str(expected_delta)) self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-"
+ str(date2) + "!=" + str(expected_delta))
def test_iso_to_struct(self): def test_iso_to_struct(self):
self.compare_dates(converters.jsdate_to_time("2013-01-01"), converters.jsdate_to_time("2012-12-31"), datetime.timedelta(days=1)) self.compare_dates(converters.jsdate_to_time("2013-01-01"),
self.compare_dates(converters.jsdate_to_time("2013-01-01T00"), converters.jsdate_to_time("2012-12-31T23"), datetime.timedelta(hours=1)) converters.jsdate_to_time("2012-12-31"),
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"), converters.jsdate_to_time("2012-12-31T23:59"), datetime.timedelta(minutes=1)) datetime.timedelta(days=1))
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"), converters.jsdate_to_time("2012-12-31T23:59:59"), datetime.timedelta(seconds=1)) self.compare_dates(converters.jsdate_to_time("2013-01-01T00"),
converters.jsdate_to_time("2012-12-31T23"),
datetime.timedelta(hours=1))
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"),
converters.jsdate_to_time("2012-12-31T23:59"),
datetime.timedelta(minutes=1))
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"),
converters.jsdate_to_time("2012-12-31T23:59:59"),
datetime.timedelta(seconds=1))
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00Z"),
converters.jsdate_to_time("2012-12-31T23:59:59Z"),
datetime.timedelta(seconds=1))
self.compare_dates(converters.jsdate_to_time("2012-12-31T23:00:01-01:00"),
converters.jsdate_to_time("2013-01-01T00:00:00+01:00"),
datetime.timedelta(hours=1, seconds=1))
class CourseTestCase(ModuleStoreTestCase): class CourseTestCase(ModuleStoreTestCase):
...@@ -104,7 +120,7 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -104,7 +120,7 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(jsondetails['effort'], "effort somehow initialized") self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
def test_update_and_fetch(self): def test_update_and_fetch(self):
## NOTE: I couldn't figure out how to validly test time setting w/ all the conversions # # NOTE: I couldn't figure out how to validly test time setting w/ all the conversions
jsondetails = CourseDetails.fetch(self.course_location) jsondetails = CourseDetails.fetch(self.course_location)
jsondetails.syllabus = "<a href='foo'>bar</a>" jsondetails.syllabus = "<a href='foo'>bar</a>"
# encode - decode to convert date fields and other data which changes form # encode - decode to convert date fields and other data which changes form
...@@ -182,7 +198,7 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -182,7 +198,7 @@ class CourseDetailsViewTest(CourseTestCase):
details_encoded = jsdate_to_time(details[field]) details_encoded = jsdate_to_time(details[field])
dt2 = ConvertersTestCase.struct_to_datetime(details_encoded) dt2 = ConvertersTestCase.struct_to_datetime(details_encoded)
expected_delta = datetime.timedelta(0) expected_delta = datetime.timedelta(0)
self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context) self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
else: else:
self.fail(field + " missing from encoded but in details at " + context) self.fail(field + " missing from encoded but in details at " + context)
...@@ -269,7 +285,7 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -269,7 +285,7 @@ class CourseMetadataEditingTest(CourseTestCase):
CourseTestCase.setUp(self) CourseTestCase.setUp(self)
# add in the full class too # add in the full class too
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
self.fullcourse_location = Location(['i4x','edX','full','course','6.002_Spring_2012', None]) self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])
def test_fetch_initial_fields(self): def test_fetch_initial_fields(self):
......
...@@ -3,19 +3,24 @@ from contentstore.utils import get_modulestore ...@@ -3,19 +3,24 @@ from contentstore.utils import get_modulestore
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope from xblock.core import Scope
from xmodule.course_module import CourseDescriptor
class CourseMetadata(object): class CourseMetadata(object):
''' '''
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones. For CRUD operations on metadata fields which do not have specific editors
The objects have no predefined attrs but instead are obj encodings of the editable metadata. on the other pages including any user generated ones.
The objects have no predefined attrs but instead are obj encodings of the
editable metadata.
''' '''
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod'] FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end',
'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod']
@classmethod @classmethod
def fetch(cls, course_location): def fetch(cls, course_location):
""" """
Fetch the key:value editable course details for the given course from persistence and return a CourseMetadata model. Fetch the key:value editable course details for the given course from
persistence and return a CourseMetadata model.
""" """
if not isinstance(course_location, Location): if not isinstance(course_location, Location):
course_location = Location(course_location) course_location = Location(course_location)
...@@ -29,7 +34,7 @@ class CourseMetadata(object): ...@@ -29,7 +34,7 @@ class CourseMetadata(object):
continue continue
if field.name not in cls.FILTERED_LIST: if field.name not in cls.FILTERED_LIST:
course[field.name] = field.read_from(descriptor) course[field.name] = field.read_json(descriptor)
return course return course
...@@ -51,22 +56,26 @@ class CourseMetadata(object): ...@@ -51,22 +56,26 @@ class CourseMetadata(object):
if hasattr(descriptor, k) and getattr(descriptor, k) != v: if hasattr(descriptor, k) and getattr(descriptor, k) != v:
dirty = True dirty = True
setattr(descriptor, k, v) value = getattr(CourseDescriptor, k).from_json(v)
setattr(descriptor, k, value)
elif hasattr(descriptor.lms, k) and getattr(descriptor.lms, k) != k: elif hasattr(descriptor.lms, k) and getattr(descriptor.lms, k) != k:
dirty = True dirty = True
setattr(descriptor.lms, k, v) value = getattr(CourseDescriptor.lms, k).from_json(v)
setattr(descriptor.lms, k, value)
if dirty: if dirty:
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor)) get_modulestore(course_location).update_metadata(course_location,
own_metadata(descriptor))
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm # Could just generate and return a course obj w/o doing any db reads,
# it persisted correctly # but I put the reads in as a means to confirm it persisted correctly
return cls.fetch(course_location) return cls.fetch(course_location)
@classmethod @classmethod
def delete_key(cls, course_location, payload): def delete_key(cls, course_location, payload):
''' '''
Remove the given metadata key(s) from the course. payload can be a single key or [key..] Remove the given metadata key(s) from the course. payload can be a
single key or [key..]
''' '''
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
...@@ -76,6 +85,7 @@ class CourseMetadata(object): ...@@ -76,6 +85,7 @@ class CourseMetadata(object):
elif hasattr(descriptor.lms, key): elif hasattr(descriptor.lms, key):
delattr(descriptor.lms, key) delattr(descriptor.lms, key)
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor)) get_modulestore(course_location).update_metadata(course_location,
own_metadata(descriptor))
return cls.fetch(course_location) return cls.fetch(course_location)
import time import time
import datetime import datetime
import re
import calendar import calendar
import dateutil.parser
tz = "{:+03d}:{:02d}".format(time.timezone / 3600, time.timezone % 3600)
def time_to_date(time_obj): def time_to_date(time_obj):
""" """
Convert a time.time_struct to a true universal time (can pass to js Date constructor) Convert a time.time_struct to a true universal time (can pass to js Date
constructor)
""" """
# TODO change to using the isoformat() function on datetime. js date can parse those
return calendar.timegm(time_obj) * 1000 return calendar.timegm(time_obj) * 1000
def time_to_isodate(source):
'''Convert to an iso date'''
if isinstance(source, time.struct_time):
return time.strftime('%Y-%m-%dT%H:%M:%S' + tz, source)
elif isinstance(source, datetime):
return source.isoformat() + tz
def jsdate_to_time(field): def jsdate_to_time(field):
""" """
Convert a universal time (iso format) or msec since epoch to a time obj Convert a universal time (iso format) or msec since epoch to a time obj
...@@ -19,8 +30,7 @@ def jsdate_to_time(field): ...@@ -19,8 +30,7 @@ def jsdate_to_time(field):
if field is None: if field is None:
return field return field
elif isinstance(field, basestring): elif isinstance(field, basestring):
# ISO format but ignores time zone assuming it's Z. d = dateutil.parser.parse(field)
d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
return d.utctimetuple() return d.utctimetuple()
elif isinstance(field, (int, long, float)): elif isinstance(field, (int, long, float)):
return time.gmtime(field / 1000) return time.gmtime(field / 1000)
......
...@@ -12,14 +12,9 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule ...@@ -12,14 +12,9 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time 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
from datetime import datetime
import json import json
import logging
import requests
import time
import copy
from xblock.core import Scope, ModelType, List, String, Object, Boolean from xblock.core import Scope, List, String, Object, Boolean
from .fields import Date from .fields import Date
...@@ -33,26 +28,27 @@ class StringOrDate(Date): ...@@ -33,26 +28,27 @@ class StringOrDate(Date):
if it doesn't parse. if it doesn't parse.
Return None if not present or invalid. Return None if not present or invalid.
""" """
if value is None:
return None
try: try:
return time.strptime(value, self.time_format) result = super(StringOrDate, self).from_json(value)
except ValueError: except ValueError:
return value return value
if result is None:
return value
else:
return result
def to_json(self, value): def to_json(self, value):
""" """
Convert a time struct to a string Convert a time struct to a string
""" """
if value is None:
return None
try: try:
return time.strftime(self.time_format, value) result = super(StringOrDate, self).to_json(value)
except (ValueError, TypeError): except:
return value return value
if result is None:
return value
else:
return result
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False, edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
...@@ -60,6 +56,7 @@ edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False, ...@@ -60,6 +56,7 @@ edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
_cached_toc = {} _cached_toc = {}
class Textbook(object): class Textbook(object):
def __init__(self, title, book_url): def __init__(self, title, book_url):
self.title = title self.title = title
...@@ -367,7 +364,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -367,7 +364,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
textbooks.append((textbook.get('title'), textbook.get('book_url'))) textbooks.append((textbook.get('title'), textbook.get('book_url')))
xml_object.remove(textbook) xml_object.remove(textbook)
#Load the wiki tag if it exists # Load the wiki tag if it exists
wiki_slug = None wiki_slug = None
wiki_tag = xml_object.find("wiki") wiki_tag = xml_object.find("wiki")
if wiki_tag is not None: if wiki_tag is not None:
...@@ -675,7 +672,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -675,7 +672,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
# *end* of the same day, not the same time. It's going to be used as the # *end* of the same day, not the same time. It's going to be used as the
# end of the exam overall, so we don't want the exam to disappear too soon. # end of the exam overall, so we don't want the exam to disappear too soon.
# It's also used optionally as the registration end date, so time matters there too. # It's also used optionally as the registration end date, so time matters there too.
self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
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 time.gmtime(0) self.registration_start_date = self._try_parse_time('Registration_Start_Date') or time.gmtime(0)
......
...@@ -4,27 +4,33 @@ import re ...@@ -4,27 +4,33 @@ import re
from datetime import timedelta from datetime import timedelta
from xblock.core import ModelType from xblock.core import ModelType
import datetime
import dateutil.parser
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Date(ModelType): class Date(ModelType):
time_format = "%Y-%m-%dT%H:%M" tz = "{:+03d}:{:02d}".format(time.timezone / 3600, time.timezone % 3600)
def from_json(self, value): 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
if it doesn't parse. if it doesn't parse.
Return None if not present or invalid. Return None if not present or invalid.
""" """
if value is None: if field is None:
return None return field
elif isinstance(field, basestring):
try: d = dateutil.parser.parse(field)
return time.strptime(value, self.time_format) return d.utctimetuple()
except ValueError as e: elif isinstance(field, (int, long, float)):
msg = "Field {0} has bad value '{1}': '{2}'".format( return time.gmtime(field / 1000)
self._name, value, e) elif isinstance(field, time.struct_time):
return field
else:
msg = "Field {0} has bad value '{1}'".format(
self._name, field)
log.warning(msg) log.warning(msg)
return None return None
...@@ -34,8 +40,11 @@ class Date(ModelType): ...@@ -34,8 +40,11 @@ class Date(ModelType):
""" """
if value is None: if value is None:
return None return None
if isinstance(value, time.struct_time):
return time.strftime(self.time_format, value) # struct_times are always utc
return time.strftime('%Y-%m-%dT%H:%M:%SZ', value)
elif isinstance(value, datetime.datetime):
return value.isoformat() + Date.tz
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)?)?$') 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)?)?$')
...@@ -66,4 +75,4 @@ class Timedelta(ModelType): ...@@ -66,4 +75,4 @@ class Timedelta(ModelType):
cur_value = getattr(value, attr, 0) cur_value = getattr(value, attr, 0)
if cur_value > 0: if cur_value > 0:
values.append("%d %s" % (cur_value, attr)) values.append("%d %s" % (cur_value, attr))
return ' '.join(values) return ' '.join(values)
\ No newline at end of file
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